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.
This commit is contained in:
parent
4a7562d28f
commit
d59512f6f4
@ -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
|
||||
|
56
lib/http.js
56
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;
|
||||
|
67
test/simple/test-http-expect-continue.js
Normal file
67
test/simple/test-http-expect-continue.js
Normal file
@ -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…
x
Reference in New Issue
Block a user