Created
February 8, 2011 06:54
-
-
Save creationix/816004 to your computer and use it in GitHub Desktop.
Add mutable, implicit headers for easy middleware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From 761f0f03b16f4df0112f4990c38b4f59b2786d04 Mon Sep 17 00:00:00 2001 | |
From: Tim Caswell <[email protected]> | |
Date: Thu, 10 Feb 2011 02:18:13 -0800 | |
Subject: [PATCH] Add support for mutable/implicit headers for http. | |
This works for both ServerResponse and ClientRequest. | |
Adds three new methods as a couple properties to to OutgoingMessage objects. | |
Tests by Charlie Robbins. | |
Change-Id: Ib6f3829798e8f11dd2b6136e61df254f1564807e | |
--- | |
doc/api/http.markdown | 58 ++++++++++++++- | |
lib/http.js | 104 ++++++++++++++++++++++++-- | |
test/simple/test-http-mutable-headers.js | 119 ++++++++++++++++++++++++++++++ | |
3 files changed, 269 insertions(+), 12 deletions(-) | |
create mode 100644 test/simple/test-http-mutable-headers.js | |
diff --git a/doc/api/http.markdown b/doc/api/http.markdown | |
index 53476d7..6f68eeb 100644 | |
--- a/doc/api/http.markdown | |
+++ b/doc/api/http.markdown | |
@@ -261,10 +261,59 @@ Example: | |
This method must only be called once on a message and it must | |
be called before `response.end()` is called. | |
+If you call `response.write()` or `response.end()` before calling this, the | |
+implicit/mutable headers will be calculated and call this function for you. | |
+ | |
+### response.statusCode | |
+ | |
+When using implicit headers (not calling `response.writeHead()` explicitly), this property | |
+controlls the status code that will be send to the client when the headers get | |
+flushed. | |
+ | |
+Example: | |
+ | |
+ response.statusCode = 404; | |
+ | |
+### response.setHeader(name, value) | |
+ | |
+Sets a single header value for implicit headers. If this header already exists | |
+in the to-be-sent headers, it's value will be replaced. Use an array of strings | |
+here if you need to send multiple headers with the same name. | |
+ | |
+Example: | |
+ | |
+ response.setHeader("Content-Type", "text/html"); | |
+ | |
+or | |
+ | |
+ response.setHeader("Set-Cookie", ["type=ninja", "language=javascript"]); | |
+ | |
+ | |
+### response.getHeader(name) | |
+ | |
+Reads out a header that's already been queued but not sent to the client. Note | |
+that the name is case insensitive. This can only be called before headers get | |
+implicitly flushed. | |
+ | |
+Example: | |
+ | |
+ var contentType = response.getHeader('content-type'); | |
+ | |
+### response.removeHeader(name) | |
+ | |
+Removes a header that's queued for implicit sending. | |
+ | |
+Example: | |
+ | |
+ response.removeHeader("Content-Encoding"); | |
+ | |
+ | |
### response.write(chunk, encoding='utf8') | |
-This method must be called after `writeHead` was | |
-called. It sends a chunk of the response body. This method may | |
+If this method is called and `response.writeHead()` has not been called, it will | |
+switch to implicit header mode and flush the implicit headers. | |
+ | |
+This sends a chunk of the response body. This method may | |
be called multiple times to provide successive parts of the body. | |
`chunk` can be a string or a buffer. If `chunk` is a string, | |
@@ -436,7 +485,10 @@ A queue of requests waiting to be sent to sockets. | |
## http.ClientRequest | |
This object is created internally and returned from `http.request()`. It | |
-represents an _in-progress_ request whose header has already been sent. | |
+represents an _in-progress_ request whose header has already been queued. The | |
+header is still mutable using the `setHeader(name, value)`, `getHeader(name)`, | |
+`removeHeader(name)` API. The actual header will be sent along with the first | |
+data chunk or when closing the connection. | |
To get the response, add a listener for `'response'` to the request object. | |
`'response'` will be emitted from the request object when the response | |
diff --git a/lib/http.js b/lib/http.js | |
index e373166..9506a8c 100644 | |
--- a/lib/http.js | |
+++ b/lib/http.js | |
@@ -303,6 +303,9 @@ function OutgoingMessage() { | |
this._trailer = ''; | |
this.finished = false; | |
+ | |
+ this._headers = {}; | |
+ this._headerNames = {}; | |
} | |
util.inherits(OutgoingMessage, stream.Stream); | |
@@ -432,7 +435,6 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) { | |
} else if (expectExpression.test(field)) { | |
sentExpect = true; | |
- | |
} | |
} | |
@@ -495,9 +497,68 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) { | |
}; | |
+OutgoingMessage.prototype.setHeader = function(name, value) { | |
+ if (arguments.length < 2) { | |
+ throw new Error("`name` and `value` are required for setHeader()."); | |
+ } | |
+ | |
+ if (this._header) { | |
+ throw new Error("Can't set headers after they are sent."); | |
+ } | |
+ | |
+ var key = name.toLowerCase(); | |
+ this._headers[key] = value; | |
+ this._headerNames[key] = name; | |
+}; | |
+ | |
+ | |
+OutgoingMessage.prototype.getHeader = function(name) { | |
+ if (arguments.length < 1) { | |
+ throw new Error("`name` is required for getHeader()."); | |
+ } | |
+ | |
+ if (this._header) { | |
+ throw new Error("Can't use mutable header APIs after sent."); | |
+ } | |
+ | |
+ var key = name.toLowerCase(); | |
+ return this._headers[key]; | |
+}; | |
+ | |
+ | |
+OutgoingMessage.prototype.removeHeader = function(name) { | |
+ if (arguments.length < 1) { | |
+ throw new Error("`name` is required for removeHeader()."); | |
+ } | |
+ | |
+ if (this._header) { | |
+ throw new Error("Can't remove headers after they are sent."); | |
+ } | |
+ | |
+ var key = name.toLowerCase(); | |
+ delete this._headers[key]; | |
+ delete this._headerNames[key]; | |
+}; | |
+ | |
+ | |
+OutgoingMessage.prototype._renderHeaders = function() { | |
+ if (this._header) { | |
+ throw new Error("Can't render headers after they are sent to the client."); | |
+ } | |
+ var headers = {}; | |
+ var keys = Object.keys(this._headers); | |
+ for (var i = 0, l = keys.length; i < l; i++) { | |
+ var key = keys[i]; | |
+ headers[this._headerNames[key]] = this._headers[key]; | |
+ } | |
+ return headers; | |
+}; | |
+ | |
+ | |
+ | |
OutgoingMessage.prototype.write = function(chunk, encoding) { | |
if (!this._header) { | |
- throw new Error('You have to call writeHead() before write()'); | |
+ this._implicitHeader(); | |
} | |
if (!this._hasBody) { | |
@@ -557,6 +618,10 @@ OutgoingMessage.prototype.addTrailers = function(headers) { | |
OutgoingMessage.prototype.end = function(data, encoding) { | |
+ if (!this._header) { | |
+ this._implicitHeader(); | |
+ } | |
+ | |
var ret; | |
var hot = this._headerSent === false && | |
@@ -681,12 +746,16 @@ util.inherits(ServerResponse, OutgoingMessage); | |
exports.ServerResponse = ServerResponse; | |
+ServerResponse.prototype.statusCode = 200; | |
ServerResponse.prototype.writeContinue = function() { | |
this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii'); | |
this._sent100 = true; | |
}; | |
+ServerResponse.prototype._implicitHeader = function() { | |
+ this.writeHead(this.statusCode, this._renderHeaders()); | |
+}; | |
ServerResponse.prototype.writeHead = function(statusCode) { | |
var reasonPhrase, headers, headerIndex; | |
@@ -742,12 +811,21 @@ function ClientRequest(options) { | |
OutgoingMessage.call(this); | |
var method = this.method = (options.method || 'GET').toUpperCase(); | |
- var path = options.path || '/'; | |
- var headers = options.headers || {}; | |
- | |
- // Host header set by default. | |
- if (options.host && !(headers.host || headers.Host || headers.HOST)) { | |
- headers.Host = options.host; | |
+ this.path = options.path || '/'; | |
+ | |
+ if (!Array.isArray(headers)) { | |
+ if (options.headers) { | |
+ var headers = options.headers; | |
+ var keys = Object.keys(headers); | |
+ for (var i = 0, l = keys.length; i < l; i++) { | |
+ var key = keys[i]; | |
+ this.setHeader(key, headers[key]); | |
+ } | |
+ } | |
+ // Host header set by default. | |
+ if (options.host && !this.getHeader('host')) { | |
+ this.setHeader("Host", options.host); | |
+ } | |
} | |
this.shouldKeepAlive = false; | |
@@ -761,13 +839,21 @@ function ClientRequest(options) { | |
// specified. | |
this._last = true; | |
- this._storeHeader(method + ' ' + path + ' HTTP/1.1\r\n', headers); | |
+ if (Array.isArray(headers)) { | |
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', headers); | |
+ } else if (this.getHeader('expect')) { | |
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', this._renderHeaders()); | |
+ } | |
+ | |
} | |
util.inherits(ClientRequest, OutgoingMessage); | |
exports.ClientRequest = ClientRequest; | |
+ClientRequest.prototype._implicitHeader = function() { | |
+ this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', this._renderHeaders()); | |
+} | |
ClientRequest.prototype.abort = function() { | |
if (this._queue) { | |
diff --git a/test/simple/test-http-mutable-headers.js b/test/simple/test-http-mutable-headers.js | |
new file mode 100644 | |
index 0000000..8b44cd0 | |
--- /dev/null | |
+++ b/test/simple/test-http-mutable-headers.js | |
@@ -0,0 +1,119 @@ | |
+var common = require('../common'); | |
+var assert = require('assert'); | |
+var http = require('http'); | |
+ | |
+// Simple test of Node's HTTP Client mutable headers | |
+// OutgoingMessage.prototype.setHeader(name, value) | |
+// OutgoingMessage.prototype.getHeader(name) | |
+// OutgoingMessage.prototype.removeHeader(name, value) | |
+// ServerResponse.prototype.statusCode | |
+// <ClientRequest>.method | |
+// <ClientRequest>.path | |
+ | |
+var testsComplete = 0; | |
+var test = 'headers'; | |
+var content = 'hello world\n'; | |
+var cookies = [ | |
+ 'session_token=; path=/; expires=Sun, 15-Sep-2030 13:48:52 GMT', | |
+ 'prefers_open_id=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT' | |
+]; | |
+ | |
+var s = http.createServer(function(req, res) { | |
+ switch (test) { | |
+ case 'headers': | |
+ assert.throws(function () { res.setHeader() }); | |
+ assert.throws(function () { res.setHeader('someHeader') }); | |
+ assert.throws(function () { res.getHeader() }); | |
+ assert.throws(function () { res.removeHeader() }); | |
+ | |
+ res.setHeader('x-test-header', 'testing'); | |
+ res.setHeader('X-TEST-HEADER2', 'testing'); | |
+ res.setHeader('set-cookie', cookies); | |
+ res.setHeader('x-test-array-header', [1, 2, 3]); | |
+ | |
+ var val1 = res.getHeader('x-test-header'); | |
+ var val2 = res.getHeader('x-test-header2'); | |
+ assert.equal(val1, 'testing'); | |
+ assert.equal(val2, 'testing'); | |
+ | |
+ res.removeHeader('x-test-header2'); | |
+ break; | |
+ | |
+ case 'contentLength': | |
+ res.setHeader('content-length', content.length); | |
+ assert.equal(content.length, res.getHeader('Content-Length')); | |
+ break; | |
+ | |
+ case 'transferEncoding': | |
+ res.setHeader('transfer-encoding', 'chunked'); | |
+ assert.equal(res.getHeader('Transfer-Encoding'), 'chunked'); | |
+ break; | |
+ } | |
+ | |
+ res.statusCode = 201; | |
+ res.end(content); | |
+}); | |
+ | |
+s.listen(common.PORT, nextTest); | |
+ | |
+ | |
+function nextTest () { | |
+ if (test === 'end') { | |
+ return s.close(); | |
+ } | |
+ | |
+ var bufferedResponse = ''; | |
+ | |
+ http.get({ port: common.PORT }, function(response) { | |
+ console.log('TEST: ' + test); | |
+ console.log('STATUS: ' + response.statusCode); | |
+ console.log('HEADERS: '); | |
+ console.dir(response.headers); | |
+ | |
+ switch (test) { | |
+ case 'headers': | |
+ assert.equal(response.statusCode, 201); | |
+ assert.equal(response.headers['x-test-header'], | |
+ 'testing'); | |
+ assert.equal(response.headers['x-test-array-header'], | |
+ [1,2,3].join(', ')); | |
+ assert.deepEqual(cookies, | |
+ response.headers['set-cookie']); | |
+ assert.equal(response.headers['x-test-header2'] !== undefined, false); | |
+ // Make the next request | |
+ test = 'contentLength'; | |
+ console.log('foobar'); | |
+ break; | |
+ | |
+ case 'contentLength': | |
+ assert.equal(response.headers['content-length'], content.length); | |
+ test = 'transferEncoding'; | |
+ break; | |
+ | |
+ case 'transferEncoding': | |
+ assert.equal(response.headers['transfer-encoding'], 'chunked'); | |
+ test = 'end'; | |
+ break; | |
+ | |
+ default: | |
+ throw Error("?"); | |
+ } | |
+ | |
+ response.setEncoding('utf8'); | |
+ response.on('data', function(s) { | |
+ bufferedResponse += s; | |
+ }); | |
+ | |
+ response.on('end', function() { | |
+ assert.equal(content, bufferedResponse); | |
+ testsComplete++; | |
+ nextTest(); | |
+ }); | |
+ }); | |
+} | |
+ | |
+ | |
+process.on('exit', function() { | |
+ assert.equal(3, testsComplete); | |
+}); | |
+ | |
-- | |
1.7.3.5 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It's in. Success!