diff --git a/doc/api.markdown b/doc/api.markdown index 0b0ecee9fb..e5109c296b 100644 --- a/doc/api.markdown +++ b/doc/api.markdown @@ -1894,6 +1894,24 @@ header information and the first body to the client. The second time data, and sends that separately. That is, the response is buffered up to the first chunk of body. +### response.addTrailers(headers) + +This method adds HTTP trailing headers (a header but at the end of the +message) to the response. + +Trailers will **only** be emitted if chunked encoding is used for the +response; if it is not (e.g., if the request was HTTP/1.0), they will +be silently discarded. + +Note that HTTP requires the `Trailer` header to be sent if you intend to +emit trailers, with a list of the header fields in its value. E.g., + + response.writeHead(200, { 'Content-Type': 'text/plain', + 'Trailer': 'TraceInfo' }); + response.write(fileData); + response.addTrailers({'Content-MD5': "7895bf4b8828b55ceaf47747b4bca667"}); + response.end(); + ### response.end([data], [encoding]) diff --git a/lib/http.js b/lib/http.js index ea0ce2afc2..3f9d8fcad3 100644 --- a/lib/http.js +++ b/lib/http.js @@ -292,6 +292,7 @@ function OutgoingMessage (socket) { this.useChunkedEncodingByDefault = true; this._hasBody = true; + this._trailer = ''; this.finished = false; } @@ -484,6 +485,26 @@ OutgoingMessage.prototype.write = function (chunk, encoding) { return ret; }; + +OutgoingMessage.prototype.addTrailers = function (headers) { + this._trailer = ""; + var keys = Object.keys(headers); + var isArray = (Array.isArray(headers)); + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + if (isArray) { + field = headers[key][0]; + value = headers[key][1]; + } else { + field = key; + value = headers[key]; + } + + this._trailer += field + ": " + value + CRLF; + } +}; + + OutgoingMessage.prototype.finish = function () { throw new Error("finish() has been renamed to close()."); }; @@ -520,7 +541,9 @@ OutgoingMessage.prototype.end = function (data, encoding) { + l + CRLF + data - + "\r\n0\r\n\r\n" + + "\r\n0\r\n" + + this._trailer + + "\r\n" , encoding ); } else { @@ -535,7 +558,7 @@ OutgoingMessage.prototype.end = function (data, encoding) { if (!hot) { if (this.chunkedEncoding) { - ret = this._send('0\r\n\r\n'); // Last chunk. + ret = this._send('0\r\n' + this._trailer + '\r\n'); // Last chunk. } else if (!data) { // Force a flush, HACK. ret = this._send(''); diff --git a/test/simple/test-http-set-trailers.js b/test/simple/test-http-set-trailers.js new file mode 100644 index 0000000000..6b6a34c8df --- /dev/null +++ b/test/simple/test-http-set-trailers.js @@ -0,0 +1,75 @@ +common = require("../common"); +assert = common.assert; +http = require("http"); +net = require("net"); + +outstanding_reqs = 0; + +var server = http.createServer(function(req, res) { + res.writeHead(200, [ ['content-type', 'text/plain'] ]); + res.addTrailers({"x-foo": "bar"}); + res.end("stuff" + "\n"); +}); +server.listen(common.PORT); + + +// first, we test an HTTP/1.0 request. +server.addListener("listening", function() { + var c = net.createConnection(common.PORT); + var res_buffer = ""; + + c.setEncoding("utf8"); + + c.addListener("connect", function () { + outstanding_reqs++; + c.write( "GET / HTTP/1.0\r\n\r\n" ); + }); + + c.addListener("data", function (chunk) { +// console.log(chunk); + res_buffer += chunk; + }); + + c.addListener("end", function () { + c.end(); + assert.ok(! /x-foo/.test(res_buffer), "Trailer in HTTP/1.0 response."); + outstanding_reqs--; + if (outstanding_reqs == 0) { + server.close(); + process.exit(); + } + }); +}); + +// now, we test an HTTP/1.1 request. +server.addListener("listening", function() { + var c = net.createConnection(common.PORT); + var res_buffer = ""; + var tid; + + c.setEncoding("utf8"); + + c.addListener("connect", function () { + outstanding_reqs++; + c.write( "GET / HTTP/1.1\r\n\r\n" ); + tid = setTimeout(assert.fail, 2000, "Couldn't find last chunk."); + }); + + c.addListener("data", function (chunk) { +// console.log(chunk); + res_buffer += chunk; + if (/0\r\n/.test(res_buffer)) { // got the end. + outstanding_reqs--; + clearTimeout(tid); + assert.ok( + /0\r\nx-foo: bar\r\n\r\n$/.test(res_buffer), + "No trailer in HTTP/1.1 response." + ); + if (outstanding_reqs == 0) { + server.close(); + process.exit(); + } + } + }); + +});