Browse Source

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.
v0.7.4-release
Mark Nottingham 14 years ago
committed by Ryan Dahl
parent
commit
d59512f6f4
  1. 35
      doc/api.markdown
  2. 54
      lib/http.js
  3. 67
      test/simple/test-http-expect-continue.js

35
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

54
lib/http.js

@ -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!)
};
}
@ -953,6 +993,12 @@ Client.prototype._initParser = function () {
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;
}

67
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();
}
});
});
});
Loading…
Cancel
Save