Skip to content

Commit b09c588

Browse files
creationixry
authored andcommitted
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
1 parent 01a864a commit b09c588

File tree

3 files changed

+269
-12
lines changed

3 files changed

+269
-12
lines changed

doc/api/http.markdown

+55-3
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,59 @@ Example:
261261
This method must only be called once on a message and it must
262262
be called before `response.end()` is called.
263263

264+
If you call `response.write()` or `response.end()` before calling this, the
265+
implicit/mutable headers will be calculated and call this function for you.
266+
267+
### response.statusCode
268+
269+
When using implicit headers (not calling `response.writeHead()` explicitly), this property
270+
controlls the status code that will be send to the client when the headers get
271+
flushed.
272+
273+
Example:
274+
275+
response.statusCode = 404;
276+
277+
### response.setHeader(name, value)
278+
279+
Sets a single header value for implicit headers. If this header already exists
280+
in the to-be-sent headers, it's value will be replaced. Use an array of strings
281+
here if you need to send multiple headers with the same name.
282+
283+
Example:
284+
285+
response.setHeader("Content-Type", "text/html");
286+
287+
or
288+
289+
response.setHeader("Set-Cookie", ["type=ninja", "language=javascript"]);
290+
291+
292+
### response.getHeader(name)
293+
294+
Reads out a header that's already been queued but not sent to the client. Note
295+
that the name is case insensitive. This can only be called before headers get
296+
implicitly flushed.
297+
298+
Example:
299+
300+
var contentType = response.getHeader('content-type');
301+
302+
### response.removeHeader(name)
303+
304+
Removes a header that's queued for implicit sending.
305+
306+
Example:
307+
308+
response.removeHeader("Content-Encoding");
309+
310+
264311
### response.write(chunk, encoding='utf8')
265312

266-
This method must be called after `writeHead` was
267-
called. It sends a chunk of the response body. This method may
313+
If this method is called and `response.writeHead()` has not been called, it will
314+
switch to implicit header mode and flush the implicit headers.
315+
316+
This sends a chunk of the response body. This method may
268317
be called multiple times to provide successive parts of the body.
269318

270319
`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.
436485
## http.ClientRequest
437486

438487
This object is created internally and returned from `http.request()`. It
439-
represents an _in-progress_ request whose header has already been sent.
488+
represents an _in-progress_ request whose header has already been queued. The
489+
header is still mutable using the `setHeader(name, value)`, `getHeader(name)`,
490+
`removeHeader(name)` API. The actual header will be sent along with the first
491+
data chunk or when closing the connection.
440492

441493
To get the response, add a listener for `'response'` to the request object.
442494
`'response'` will be emitted from the request object when the response

lib/http.js

+95-9
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ function OutgoingMessage() {
303303
this._trailer = '';
304304

305305
this.finished = false;
306+
307+
this._headers = {};
308+
this._headerNames = {};
306309
}
307310
util.inherits(OutgoingMessage, stream.Stream);
308311

@@ -432,7 +435,6 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
432435

433436
} else if (expectExpression.test(field)) {
434437
sentExpect = true;
435-
436438
}
437439
}
438440

@@ -495,9 +497,68 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
495497
};
496498

497499

500+
OutgoingMessage.prototype.setHeader = function(name, value) {
501+
if (arguments.length < 2) {
502+
throw new Error("`name` and `value` are required for setHeader().");
503+
}
504+
505+
if (this._header) {
506+
throw new Error("Can't set headers after they are sent.");
507+
}
508+
509+
var key = name.toLowerCase();
510+
this._headers[key] = value;
511+
this._headerNames[key] = name;
512+
};
513+
514+
515+
OutgoingMessage.prototype.getHeader = function(name) {
516+
if (arguments.length < 1) {
517+
throw new Error("`name` is required for getHeader().");
518+
}
519+
520+
if (this._header) {
521+
throw new Error("Can't use mutable header APIs after sent.");
522+
}
523+
524+
var key = name.toLowerCase();
525+
return this._headers[key];
526+
};
527+
528+
529+
OutgoingMessage.prototype.removeHeader = function(name) {
530+
if (arguments.length < 1) {
531+
throw new Error("`name` is required for removeHeader().");
532+
}
533+
534+
if (this._header) {
535+
throw new Error("Can't remove headers after they are sent.");
536+
}
537+
538+
var key = name.toLowerCase();
539+
delete this._headers[key];
540+
delete this._headerNames[key];
541+
};
542+
543+
544+
OutgoingMessage.prototype._renderHeaders = function() {
545+
if (this._header) {
546+
throw new Error("Can't render headers after they are sent to the client.");
547+
}
548+
var headers = {};
549+
var keys = Object.keys(this._headers);
550+
for (var i = 0, l = keys.length; i < l; i++) {
551+
var key = keys[i];
552+
headers[this._headerNames[key]] = this._headers[key];
553+
}
554+
return headers;
555+
};
556+
557+
558+
498559
OutgoingMessage.prototype.write = function(chunk, encoding) {
499560
if (!this._header) {
500-
throw new Error('You have to call writeHead() before write()');
561+
this._implicitHeader();
501562
}
502563

503564
if (!this._hasBody) {
@@ -557,6 +618,10 @@ OutgoingMessage.prototype.addTrailers = function(headers) {
557618

558619

559620
OutgoingMessage.prototype.end = function(data, encoding) {
621+
if (!this._header) {
622+
this._implicitHeader();
623+
}
624+
560625
var ret;
561626

562627
var hot = this._headerSent === false &&
@@ -681,12 +746,16 @@ util.inherits(ServerResponse, OutgoingMessage);
681746

682747
exports.ServerResponse = ServerResponse;
683748

749+
ServerResponse.prototype.statusCode = 200;
684750

685751
ServerResponse.prototype.writeContinue = function() {
686752
this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii');
687753
this._sent100 = true;
688754
};
689755

756+
ServerResponse.prototype._implicitHeader = function() {
757+
this.writeHead(this.statusCode, this._renderHeaders());
758+
};
690759

691760
ServerResponse.prototype.writeHead = function(statusCode) {
692761
var reasonPhrase, headers, headerIndex;
@@ -742,12 +811,21 @@ function ClientRequest(options) {
742811
OutgoingMessage.call(this);
743812

744813
var method = this.method = (options.method || 'GET').toUpperCase();
745-
var path = options.path || '/';
746-
var headers = options.headers || {};
747-
748-
// Host header set by default.
749-
if (options.host && !(headers.host || headers.Host || headers.HOST)) {
750-
headers.Host = options.host;
814+
this.path = options.path || '/';
815+
816+
if (!Array.isArray(headers)) {
817+
if (options.headers) {
818+
var headers = options.headers;
819+
var keys = Object.keys(headers);
820+
for (var i = 0, l = keys.length; i < l; i++) {
821+
var key = keys[i];
822+
this.setHeader(key, headers[key]);
823+
}
824+
}
825+
// Host header set by default.
826+
if (options.host && !this.getHeader('host')) {
827+
this.setHeader("Host", options.host);
828+
}
751829
}
752830

753831
this.shouldKeepAlive = false;
@@ -761,13 +839,21 @@ function ClientRequest(options) {
761839
// specified.
762840
this._last = true;
763841

764-
this._storeHeader(method + ' ' + path + ' HTTP/1.1\r\n', headers);
842+
if (Array.isArray(headers)) {
843+
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', headers);
844+
} else if (this.getHeader('expect')) {
845+
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', this._renderHeaders());
846+
}
847+
765848
}
766849
util.inherits(ClientRequest, OutgoingMessage);
767850

768851

769852
exports.ClientRequest = ClientRequest;
770853

854+
ClientRequest.prototype._implicitHeader = function() {
855+
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', this._renderHeaders());
856+
}
771857

772858
ClientRequest.prototype.abort = function() {
773859
if (this._queue) {
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
var common = require('../common');
2+
var assert = require('assert');
3+
var http = require('http');
4+
5+
// Simple test of Node's HTTP Client mutable headers
6+
// OutgoingMessage.prototype.setHeader(name, value)
7+
// OutgoingMessage.prototype.getHeader(name)
8+
// OutgoingMessage.prototype.removeHeader(name, value)
9+
// ServerResponse.prototype.statusCode
10+
// <ClientRequest>.method
11+
// <ClientRequest>.path
12+
13+
var testsComplete = 0;
14+
var test = 'headers';
15+
var content = 'hello world\n';
16+
var cookies = [
17+
'session_token=; path=/; expires=Sun, 15-Sep-2030 13:48:52 GMT',
18+
'prefers_open_id=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT'
19+
];
20+
21+
var s = http.createServer(function(req, res) {
22+
switch (test) {
23+
case 'headers':
24+
assert.throws(function () { res.setHeader() });
25+
assert.throws(function () { res.setHeader('someHeader') });
26+
assert.throws(function () { res.getHeader() });
27+
assert.throws(function () { res.removeHeader() });
28+
29+
res.setHeader('x-test-header', 'testing');
30+
res.setHeader('X-TEST-HEADER2', 'testing');
31+
res.setHeader('set-cookie', cookies);
32+
res.setHeader('x-test-array-header', [1, 2, 3]);
33+
34+
var val1 = res.getHeader('x-test-header');
35+
var val2 = res.getHeader('x-test-header2');
36+
assert.equal(val1, 'testing');
37+
assert.equal(val2, 'testing');
38+
39+
res.removeHeader('x-test-header2');
40+
break;
41+
42+
case 'contentLength':
43+
res.setHeader('content-length', content.length);
44+
assert.equal(content.length, res.getHeader('Content-Length'));
45+
break;
46+
47+
case 'transferEncoding':
48+
res.setHeader('transfer-encoding', 'chunked');
49+
assert.equal(res.getHeader('Transfer-Encoding'), 'chunked');
50+
break;
51+
}
52+
53+
res.statusCode = 201;
54+
res.end(content);
55+
});
56+
57+
s.listen(common.PORT, nextTest);
58+
59+
60+
function nextTest () {
61+
if (test === 'end') {
62+
return s.close();
63+
}
64+
65+
var bufferedResponse = '';
66+
67+
http.get({ port: common.PORT }, function(response) {
68+
console.log('TEST: ' + test);
69+
console.log('STATUS: ' + response.statusCode);
70+
console.log('HEADERS: ');
71+
console.dir(response.headers);
72+
73+
switch (test) {
74+
case 'headers':
75+
assert.equal(response.statusCode, 201);
76+
assert.equal(response.headers['x-test-header'],
77+
'testing');
78+
assert.equal(response.headers['x-test-array-header'],
79+
[1,2,3].join(', '));
80+
assert.deepEqual(cookies,
81+
response.headers['set-cookie']);
82+
assert.equal(response.headers['x-test-header2'] !== undefined, false);
83+
// Make the next request
84+
test = 'contentLength';
85+
console.log('foobar');
86+
break;
87+
88+
case 'contentLength':
89+
assert.equal(response.headers['content-length'], content.length);
90+
test = 'transferEncoding';
91+
break;
92+
93+
case 'transferEncoding':
94+
assert.equal(response.headers['transfer-encoding'], 'chunked');
95+
test = 'end';
96+
break;
97+
98+
default:
99+
throw Error("?");
100+
}
101+
102+
response.setEncoding('utf8');
103+
response.on('data', function(s) {
104+
bufferedResponse += s;
105+
});
106+
107+
response.on('end', function() {
108+
assert.equal(content, bufferedResponse);
109+
testsComplete++;
110+
nextTest();
111+
});
112+
});
113+
}
114+
115+
116+
process.on('exit', function() {
117+
assert.equal(3, testsComplete);
118+
});
119+

0 commit comments

Comments
 (0)