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
This commit is contained in:
parent
01a864a29d
commit
b09c5889be
@ -261,10 +261,59 @@ Example:
|
|||||||
This method must only be called once on a message and it must
|
This method must only be called once on a message and it must
|
||||||
be called before `response.end()` is called.
|
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')
|
### response.write(chunk, encoding='utf8')
|
||||||
|
|
||||||
This method must be called after `writeHead` was
|
If this method is called and `response.writeHead()` has not been called, it will
|
||||||
called. It sends a chunk of the response body. This method may
|
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.
|
be called multiple times to provide successive parts of the body.
|
||||||
|
|
||||||
`chunk` can be a string or a buffer. If `chunk` is a string,
|
`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
|
## http.ClientRequest
|
||||||
|
|
||||||
This object is created internally and returned from `http.request()`. It
|
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.
|
To get the response, add a listener for `'response'` to the request object.
|
||||||
`'response'` will be emitted from the request object when the response
|
`'response'` will be emitted from the request object when the response
|
||||||
|
100
lib/http.js
100
lib/http.js
@ -303,6 +303,9 @@ function OutgoingMessage() {
|
|||||||
this._trailer = '';
|
this._trailer = '';
|
||||||
|
|
||||||
this.finished = false;
|
this.finished = false;
|
||||||
|
|
||||||
|
this._headers = {};
|
||||||
|
this._headerNames = {};
|
||||||
}
|
}
|
||||||
util.inherits(OutgoingMessage, stream.Stream);
|
util.inherits(OutgoingMessage, stream.Stream);
|
||||||
|
|
||||||
@ -432,7 +435,6 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
|
|||||||
|
|
||||||
} else if (expectExpression.test(field)) {
|
} else if (expectExpression.test(field)) {
|
||||||
sentExpect = true;
|
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) {
|
OutgoingMessage.prototype.write = function(chunk, encoding) {
|
||||||
if (!this._header) {
|
if (!this._header) {
|
||||||
throw new Error('You have to call writeHead() before write()');
|
this._implicitHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._hasBody) {
|
if (!this._hasBody) {
|
||||||
@ -557,6 +618,10 @@ OutgoingMessage.prototype.addTrailers = function(headers) {
|
|||||||
|
|
||||||
|
|
||||||
OutgoingMessage.prototype.end = function(data, encoding) {
|
OutgoingMessage.prototype.end = function(data, encoding) {
|
||||||
|
if (!this._header) {
|
||||||
|
this._implicitHeader();
|
||||||
|
}
|
||||||
|
|
||||||
var ret;
|
var ret;
|
||||||
|
|
||||||
var hot = this._headerSent === false &&
|
var hot = this._headerSent === false &&
|
||||||
@ -681,12 +746,16 @@ util.inherits(ServerResponse, OutgoingMessage);
|
|||||||
|
|
||||||
exports.ServerResponse = ServerResponse;
|
exports.ServerResponse = ServerResponse;
|
||||||
|
|
||||||
|
ServerResponse.prototype.statusCode = 200;
|
||||||
|
|
||||||
ServerResponse.prototype.writeContinue = function() {
|
ServerResponse.prototype.writeContinue = function() {
|
||||||
this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii');
|
this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii');
|
||||||
this._sent100 = true;
|
this._sent100 = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ServerResponse.prototype._implicitHeader = function() {
|
||||||
|
this.writeHead(this.statusCode, this._renderHeaders());
|
||||||
|
};
|
||||||
|
|
||||||
ServerResponse.prototype.writeHead = function(statusCode) {
|
ServerResponse.prototype.writeHead = function(statusCode) {
|
||||||
var reasonPhrase, headers, headerIndex;
|
var reasonPhrase, headers, headerIndex;
|
||||||
@ -742,12 +811,21 @@ function ClientRequest(options) {
|
|||||||
OutgoingMessage.call(this);
|
OutgoingMessage.call(this);
|
||||||
|
|
||||||
var method = this.method = (options.method || 'GET').toUpperCase();
|
var method = this.method = (options.method || 'GET').toUpperCase();
|
||||||
var path = options.path || '/';
|
this.path = options.path || '/';
|
||||||
var headers = options.headers || {};
|
|
||||||
|
|
||||||
|
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.
|
// Host header set by default.
|
||||||
if (options.host && !(headers.host || headers.Host || headers.HOST)) {
|
if (options.host && !this.getHeader('host')) {
|
||||||
headers.Host = options.host;
|
this.setHeader("Host", options.host);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.shouldKeepAlive = false;
|
this.shouldKeepAlive = false;
|
||||||
@ -761,13 +839,21 @@ function ClientRequest(options) {
|
|||||||
// specified.
|
// specified.
|
||||||
this._last = true;
|
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);
|
util.inherits(ClientRequest, OutgoingMessage);
|
||||||
|
|
||||||
|
|
||||||
exports.ClientRequest = ClientRequest;
|
exports.ClientRequest = ClientRequest;
|
||||||
|
|
||||||
|
ClientRequest.prototype._implicitHeader = function() {
|
||||||
|
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', this._renderHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
ClientRequest.prototype.abort = function() {
|
ClientRequest.prototype.abort = function() {
|
||||||
if (this._queue) {
|
if (this._queue) {
|
||||||
|
119
test/simple/test-http-mutable-headers.js
Normal file
119
test/simple/test-http-mutable-headers.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user