From d59512f6f406ceae4940fd9c83e8255f0c03173b Mon Sep 17 00:00:00 2001 From: Mark Nottingham Date: Fri, 10 Sep 2010 11:56:35 +1000 Subject: [PATCH] Add support for handling Expect: 100-continue HTTP/1.1 requests, either with an event (check_continue) or automatically, if no event handler is present. Add client-side expect/continue support, tests. Expound upon client requirements for expect/continue. --- doc/api.markdown | 35 +++++++++++++ lib/http.js | 56 ++++++++++++++++++-- test/simple/test-http-expect-continue.js | 67 ++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 test/simple/test-http-expect-continue.js diff --git a/doc/api.markdown b/doc/api.markdown index 66d83ce2e5..7469cbab6c 100644 --- a/doc/api.markdown +++ b/doc/api.markdown @@ -1659,6 +1659,22 @@ This is an `EventEmitter` with the following events: Emitted each time there is request. Note that there may be multiple requests per connection (in the case of keep-alive connections). +### Event: 'checkContinue' + +`function (request, response) {}` + +Emitted each time a request with an http Expect: 100-continue is received. +If this event isn't listened for, the server will automatically respond +with a 100 Continue as appropriate. + +Handling this event involves calling `response.writeContinue` if the client +should continue to send the request body, or generating an appropriate HTTP +response (e.g., 400 Bad Request) if the client should not continue to send the +request body. + +Note that when this event is emitted and handled, the `request` event will +not be emitted. + ### Event: 'upgrade' `function (request, socket, head)` @@ -1834,6 +1850,11 @@ authentication details. This object is created internally by a HTTP server--not by the user. It is passed as the second parameter to the `'request'` event. It is a `Writable Stream`. +### response.writeContinue() + +Sends a HTTP/1.1 100 Continue message to the client, indicating that +the request body should be sent. See the the `checkContinue` event on +`Server`. ### response.writeHead(statusCode, [reasonPhrase], [headers]) @@ -1936,6 +1957,11 @@ There are a few special headers that should be noted. * Sending a 'Content-length' header will disable the default chunked encoding. +* Sending an 'Expect' header will immediately send the request headers. + Usually, when sending 'Expect: 100-continue', you should both set a timeout + and listen for the `continue` event. See RFC2616 Section 8.2.3 for more + information. + ### Event: 'upgrade' @@ -1947,6 +1973,15 @@ connections closed. See the description of the `upgrade` event for `http.Server` for further details. +### Event: 'continue' + +`function ()` + +Emitted when the server sends a '100 Continue' HTTP response, usually because +the request contained 'Expect: 100-continue'. This is an instruction that +the client should send the request body. + + ### http.createClient(port, host='localhost', secure=false, [credentials]) Constructs a new HTTP client. `port` and diff --git a/lib/http.js b/lib/http.js index 310c533ea1..2f17683516 100755 --- a/lib/http.js +++ b/lib/http.js @@ -75,7 +75,7 @@ var parsers = new FreeList('parsers', 1000, function () { parser.incoming.method = info.method; } else { // client only - parser.incoming.statusCode = info.statusCode; + parser.incoming.statusCode = info.statusCode; } parser.incoming.upgrade = info.upgrade; @@ -178,6 +178,8 @@ var transferEncodingExpression = /Transfer-Encoding/i; var closeExpression = /close/i; var chunkExpression = /chunk/i; var contentLengthExpression = /Content-Length/i; +var expectExpression = /Expect/i; +var continueExpression = /100-continue/i; /* Abstract base class for ServerRequest and ClientResponse. */ @@ -302,7 +304,6 @@ sys.inherits(OutgoingMessage, events.EventEmitter); exports.OutgoingMessage = OutgoingMessage; // This abstract either writing directly to the socket or buffering it. -// Rename to _writeRaw() ? OutgoingMessage.prototype._send = function (data, encoding) { // This is a shameful hack to get the headers and first body chunk onto // the same packet. Future versions of Node are going to take care of @@ -316,7 +317,10 @@ OutgoingMessage.prototype._send = function (data, encoding) { } this._headerSent = true; } + this._writeRaw(data, encoding) +} +OutgoingMessage.prototype._writeRaw = function(data, encoding) { if (this.connection._outgoing[0] === this && this.connection.writable) { // There might be pending data in the this.output buffer. while (this.output.length) { @@ -371,6 +375,7 @@ OutgoingMessage.prototype._storeHeader = function (firstLine, headers) { var sentConnectionHeader = false; var sentContentLengthHeader = false; var sentTransferEncodingHeader = false; + var sentExpect = false; // firstLine in the case of request is: "GET /index.html HTTP/1.1\r\n" // in the case of response it is: "HTTP/1.1 200 OK\r\n" @@ -396,6 +401,9 @@ OutgoingMessage.prototype._storeHeader = function (firstLine, headers) { } else if (contentLengthExpression.test(field)) { sentContentLengthHeader = true; + } else if (expectExpression.test(field)) { + sentExpect = true; + } } @@ -451,7 +459,11 @@ OutgoingMessage.prototype._storeHeader = function (firstLine, headers) { this._header = messageHeader + CRLF; this._headerSent = false; - // wait until the first body chunk, or close(), is sent to flush. + // wait until the first body chunk, or close(), is sent to flush, + // UNLESS we're sending Expect: 100-continue. + if (sentExpect) { + this._send(""); + } }; @@ -592,6 +604,10 @@ function ServerResponse (req) { sys.inherits(ServerResponse, OutgoingMessage); exports.ServerResponse = ServerResponse; +ServerResponse.prototype.writeContinue = function () { + this._writeRaw("HTTP/1.1 100 Continue" + CRLF + CRLF, 'ascii'); + this._sent100 = true; +} ServerResponse.prototype.writeHead = function (statusCode) { var reasonPhrase, headers, headerIndex; @@ -613,16 +629,28 @@ ServerResponse.prototype.writeHead = function (statusCode) { var statusLine = "HTTP/1.1 " + statusCode.toString() + " " + reasonPhrase + CRLF; - if (statusCode === 204 || statusCode === 304) { + if ( statusCode === 204 + || statusCode === 304 + || (statusCode >= 100 && statusCode <= 199) + ) { // RFC 2616, 10.2.5: // The 204 response MUST NOT include a message-body, and thus is always // terminated by the first empty line after the header fields. // RFC 2616, 10.3.5: // The 304 response MUST NOT contain a message-body, and thus is always // terminated by the first empty line after the header fields. + // RFC 2616, 10.1 Informational 1xx: + // This class of status code indicates a provisional response, + // consisting only of the Status-Line and optional headers, and is + // terminated by an empty line. this._hasBody = false; } + // don't keep alive connections where the client expects 100 Continue + // but we sent a final status; they may put extra bytes on the wire. + if (this._expect_continue && ! this._sent100) { + this._shouldKeepAlive = false; + } this._storeHeader(statusLine, headers); }; @@ -843,7 +871,19 @@ function connectionListener (socket) { res.shouldKeepAlive = shouldKeepAlive; socket._outgoing.push(res); - self.emit('request', req, res); + if ('expect' in req.headers + && (req.httpVersionMajor == 1 && req.httpVersionMinor == 1) + && continueExpression.test(req.headers['expect'])) { + res._expect_continue = true; + if (self.listeners("checkContinue").length) { + self.emit("checkContinue", req, res) + } else { + res.writeContinue(); + self.emit('request', req, res); + } + } else { + self.emit('request', req, res); + } return false; // Not a HEAD response. (Not even a response!) }; } @@ -952,6 +992,12 @@ Client.prototype._initParser = function () { // A major oversight in HTTP. Hence this nastiness. var isHeadResponse = req.method == "HEAD"; debug('isHeadResponse ' + isHeadResponse); + + if (res.statusCode == 100) { + // restart the parser, as this is a continue message. + req.emit("continue"); + return true; + } if (req.shouldKeepAlive && res.headers.connection === 'close') { req.shouldKeepAlive = false; diff --git a/test/simple/test-http-expect-continue.js b/test/simple/test-http-expect-continue.js new file mode 100644 index 0000000000..705b66d7dd --- /dev/null +++ b/test/simple/test-http-expect-continue.js @@ -0,0 +1,67 @@ +var common = require("../common"); +var assert = common.assert; +var sys = require("sys"); +var http = require("http"); + +var outstanding_reqs = 0; +var test_req_body = "some stuff...\n"; +var test_res_body = "other stuff!\n"; +var sent_continue = false; +var got_continue = false; + +function handler(req, res) { + assert.equal(sent_continue, true, "Full response sent before 100 Continue"); + common.debug("Server sending full response..."); + res.writeHead(200, { + 'Content-Type' : 'text/plain', + "ABCD" : "1" + }); + res.end(test_res_body); +} + +var server = http.createServer(handler); +server.addListener("checkContinue", function(req, res) { + common.debug("Server got Expect: 100-continue..."); + res.writeContinue(); + sent_continue = true; + handler(req, res); +}); +server.listen(common.PORT); + + + +server.addListener("listening", function() { + var client = http.createClient(common.PORT); + req = client.request("POST", "/world", { + "Expect": "100-continue", + }); + common.debug("Client sending request..."); + outstanding_reqs++; + body = ""; + req.addListener('continue', function () { + common.debug("Client got 100 Continue..."); + got_continue = true; + req.end(test_req_body); + }); + req.addListener('response', function (res) { + assert.equal(got_continue, true, + "Full response received before 100 Continue" + ); + assert.equal(200, res.statusCode, + "Final status code was " + res.statusCode + ", not 200." + ); + res.setEncoding("utf8"); + res.addListener('data', function (chunk) { body += chunk; }); + res.addListener('end', function () { + common.debug("Got full response."); + assert.equal(body, test_res_body, "Response body doesn't match."); +// common.debug(sys.inspect(res.headers)); + assert.ok("abcd" in res.headers, "Response headers missing."); + outstanding_reqs--; + if (outstanding_reqs == 0) { + server.close(); + process.exit(); + } + }); + }); +});