-
-
Save creationix/816004 to your computer and use it in GitHub Desktop.
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 |
The reason I made this change was because outgoing headers were not getting written correctly without it. I set an array header here (https://gist.github.com/818305#L114), then assert that it is set here (https://gist.github.com/818305#L131).
If I remove that code the value that is returned for test-array-header
is "1" not "1,2,3" and definitely not "[1, 2, 3]" (i.e. an Array). I don't know where the problem is in http.js that allows for Set-Cookie to be handled special, but the code I pointed out before doesn't seem to be functioning correctly. I get an assertion error when running the test otherwise:
$ node test/simple/test-http-mutable-headers.js STATUS: 201 assert.js:81 throw new assert.AssertionError({ ^ AssertionError: "1,2,3" == "1" at ClientRequest. (/usr/src/node/test/simple/test-http-mutable-headers.js:43:12) at ClientRequest.emit (events.js:42:17) at HTTPParser.onIncoming (http.js:1452:9) at HTTPParser.onHeadersComplete (http.js:87:31) at Client.onData [as ondata] (http.js:1331:27) at Client._onReadable (net.js:648:27) at IOWatcher.onReadable [as callback] (net.js:156:10)
I don't know this code that well, hopefully you (or maybe Ry) can shed some light on what's going on in the Outgoing headers to make this patch a little more elegant.
Disregard the previous comment for I really didn't understand the test I was writing. I'm going to blame this on lack of sleep. Basically I was setting test-array-header
to be an array, which by design should only return "1" since it is not marked with "x-". The implications of this are that we won't get multiple headers, but this is definitely a step in the right direction.
Updated test and hopefully final patch forthcoming
Ok:
- All tests pass (100% backwards compatible)
- Removed munging in renderHeaders() function that I added b/c I lost my understanding of HTTP at 6am >.<
- Good test coverage of all edge cases in test/simple/test-http-mutable-headers.js
It's in. Success!
in 0.4.0?
Indeed. Last commit before the version bump :-D nodejs/node-v0.x-archive@b09c588
I'd rather not put any value munging in the renderHeaders function. That kind of logic goes elsewhere. I saw the code where incoming multiple headers became a single header with commas, and that makes sense for that case. But for outgoing, I don't want to mess with the data any more than needed. The existing writeHeader seems to accept arrays as values. There is no reason to convert it to a single string.