http2: handle 100-continue flow & writeContinue
Adds an implementation for writeContinue based on the h2 spec & the existing http implementation. ClientHttp2Stream now also emits a continue event when it receives headers with :status 100. Includes two test cases for default server continue behaviour and for the exposed checkContinue listener. PR-URL: https://github.com/nodejs/node/pull/15039 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
parent
0f7c06eb2d
commit
ad3d2ce68a
@ -849,6 +849,15 @@ used exclusively on HTTP/2 Clients. `Http2Stream` instances on the client
|
|||||||
provide events such as `'response'` and `'push'` that are only relevant on
|
provide events such as `'response'` and `'push'` that are only relevant on
|
||||||
the client.
|
the client.
|
||||||
|
|
||||||
|
#### Event: 'continue'
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
Emitted when the server sends a `100 Continue` status, usually because
|
||||||
|
the request contained `Expect: 100-continue`. This is an instruction that
|
||||||
|
the client should send the request body.
|
||||||
|
|
||||||
#### Event: 'headers'
|
#### Event: 'headers'
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
@ -1306,6 +1315,28 @@ added: v8.4.0
|
|||||||
The `'timeout'` event is emitted when there is no activity on the Server for
|
The `'timeout'` event is emitted when there is no activity on the Server for
|
||||||
a given number of milliseconds set using `http2server.setTimeout()`.
|
a given number of milliseconds set using `http2server.setTimeout()`.
|
||||||
|
|
||||||
|
#### Event: 'checkContinue'
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* `request` {http2.Http2ServerRequest}
|
||||||
|
* `response` {http2.Http2ServerResponse}
|
||||||
|
|
||||||
|
If a [`'request'`][] listener is registered or [`'http2.createServer()'`][] is
|
||||||
|
supplied a callback function, the `'checkContinue'` event is emitted each time
|
||||||
|
a request with an HTTP `Expect: 100-continue` is received. If this event is
|
||||||
|
not listened for, the server will automatically respond with a status
|
||||||
|
`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.
|
||||||
|
|
||||||
### Class: Http2SecureServer
|
### Class: Http2SecureServer
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
@ -1389,6 +1420,28 @@ per session. See the [Compatibility API](compatiblity-api).
|
|||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
#### Event: 'checkContinue'
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
* `request` {http2.Http2ServerRequest}
|
||||||
|
* `response` {http2.Http2ServerResponse}
|
||||||
|
|
||||||
|
If a [`'request'`][] listener is registered or [`'http2.createSecureServer()'`][]
|
||||||
|
is supplied a callback function, the `'checkContinue'` event is emitted each
|
||||||
|
time a request with an HTTP `Expect: 100-continue` is received. If this event
|
||||||
|
is not listened for, the server will automatically respond with a status
|
||||||
|
`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.
|
||||||
|
|
||||||
### http2.createServer(options[, onRequestHandler])
|
### http2.createServer(options[, onRequestHandler])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
@ -2537,8 +2590,9 @@ buffer. Returns `false` if all or part of the data was queued in user memory.
|
|||||||
added: v8.4.0
|
added: v8.4.0
|
||||||
-->
|
-->
|
||||||
|
|
||||||
Throws an error as the `'continue'` flow is not current implemented. Added for
|
Sends a status `100 Continue` to the client, indicating that the request body
|
||||||
parity with [HTTP/1]().
|
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
|
||||||
|
`Http2SecureServer`.
|
||||||
|
|
||||||
### response.writeHead(statusCode[, statusMessage][, headers])
|
### response.writeHead(statusCode[, statusMessage][, headers])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
@ -2618,6 +2672,7 @@ if the stream is closed.
|
|||||||
[Settings Object]: #http2_settings_object
|
[Settings Object]: #http2_settings_object
|
||||||
[Using options.selectPadding]: #http2_using_options_selectpadding
|
[Using options.selectPadding]: #http2_using_options_selectpadding
|
||||||
[Writable Stream]: stream.html#stream_writable_streams
|
[Writable Stream]: stream.html#stream_writable_streams
|
||||||
|
[`'checkContinue'`]: #http2_event_checkcontinue
|
||||||
[`'request'`]: #http2_event_request
|
[`'request'`]: #http2_event_request
|
||||||
[`'unknownProtocol'`]: #http2_event_unknownprotocol
|
[`'unknownProtocol'`]: #http2_event_unknownprotocol
|
||||||
[`ClientHttp2Stream`]: #http2_class_clienthttp2stream
|
[`ClientHttp2Stream`]: #http2_class_clienthttp2stream
|
||||||
@ -2628,7 +2683,9 @@ if the stream is closed.
|
|||||||
[`ServerRequest`]: #http2_class_server_request
|
[`ServerRequest`]: #http2_class_server_request
|
||||||
[`TypeError`]: errors.html#errors_class_typeerror
|
[`TypeError`]: errors.html#errors_class_typeerror
|
||||||
[`http2.SecureServer`]: #http2_class_http2secureserver
|
[`http2.SecureServer`]: #http2_class_http2secureserver
|
||||||
|
[`http2.createSecureServer()`]: #http2_createsecureserver_options_onrequesthandler
|
||||||
[`http2.Server`]: #http2_class_http2server
|
[`http2.Server`]: #http2_class_http2server
|
||||||
|
[`http2.createServer()`]: #http2_createserver_options_onrequesthandler
|
||||||
[`net.Socket`]: net.html#net_class_net_socket
|
[`net.Socket`]: net.html#net_class_net_socket
|
||||||
[`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed
|
[`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed
|
||||||
[`response.end()`]: #http2_response_end_data_encoding_callback
|
[`response.end()`]: #http2_response_end_data_encoding_callback
|
||||||
@ -2636,6 +2693,7 @@ if the stream is closed.
|
|||||||
[`response.socket`]: #http2_response_socket
|
[`response.socket`]: #http2_response_socket
|
||||||
[`response.write()`]: #http2_response_write_chunk_encoding_callback
|
[`response.write()`]: #http2_response_write_chunk_encoding_callback
|
||||||
[`response.write(data, encoding)`]: http.html#http_response_write_chunk_encoding_callback
|
[`response.write(data, encoding)`]: http.html#http_response_write_chunk_encoding_callback
|
||||||
|
[`response.writeContinue()`]: #http2_response_writecontinue
|
||||||
[`response.writeHead()`]: #http2_response_writehead_statuscode_statusmessage_headers
|
[`response.writeHead()`]: #http2_response_writehead_statuscode_statusmessage_headers
|
||||||
[`stream.pushStream()`]: #http2_stream-pushstream
|
[`stream.pushStream()`]: #http2_stream-pushstream
|
||||||
[`tls.TLSSocket`]: tls.html#tls_class_tls_tlssocket
|
[`tls.TLSSocket`]: tls.html#tls_class_tls_tlssocket
|
||||||
|
@ -529,9 +529,14 @@ class Http2ServerResponse extends Stream {
|
|||||||
this.emit('finish');
|
this.emit('finish');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO doesn't support callbacks
|
||||||
writeContinue() {
|
writeContinue() {
|
||||||
// TODO mcollina check what is the continue flow
|
const stream = this[kStream];
|
||||||
throw new Error('not implemented yet');
|
if (stream === undefined) return false;
|
||||||
|
this[kStream].additionalHeaders({
|
||||||
|
[constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_CONTINUE
|
||||||
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,7 +561,7 @@ function onServerStream(stream, headers, flags) {
|
|||||||
if (server.listenerCount('checkContinue')) {
|
if (server.listenerCount('checkContinue')) {
|
||||||
server.emit('checkContinue', request, response);
|
server.emit('checkContinue', request, response);
|
||||||
} else {
|
} else {
|
||||||
response.sendContinue();
|
response.writeContinue();
|
||||||
server.emit('request', request, response);
|
server.emit('request', request, response);
|
||||||
}
|
}
|
||||||
} else if (server.listenerCount('checkExpectation')) {
|
} else if (server.listenerCount('checkExpectation')) {
|
||||||
|
@ -120,6 +120,7 @@ const {
|
|||||||
HTTP2_METHOD_HEAD,
|
HTTP2_METHOD_HEAD,
|
||||||
HTTP2_METHOD_CONNECT,
|
HTTP2_METHOD_CONNECT,
|
||||||
|
|
||||||
|
HTTP_STATUS_CONTINUE,
|
||||||
HTTP_STATUS_CONTENT_RESET,
|
HTTP_STATUS_CONTENT_RESET,
|
||||||
HTTP_STATUS_OK,
|
HTTP_STATUS_OK,
|
||||||
HTTP_STATUS_NO_CONTENT,
|
HTTP_STATUS_NO_CONTENT,
|
||||||
@ -2113,10 +2114,17 @@ class ClientHttp2Stream extends Http2Stream {
|
|||||||
this[kState].headersSent = true;
|
this[kState].headersSent = true;
|
||||||
if (id !== undefined)
|
if (id !== undefined)
|
||||||
this[kInit](id);
|
this[kInit](id);
|
||||||
|
this.on('headers', handleHeaderContinue);
|
||||||
debug(`[${sessionName(session[kType])}] clienthttp2stream created`);
|
debug(`[${sessionName(session[kType])}] clienthttp2stream created`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleHeaderContinue(headers) {
|
||||||
|
if (headers[HTTP2_HEADER_STATUS] === HTTP_STATUS_CONTINUE) {
|
||||||
|
this.emit('continue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setTimeout = {
|
const setTimeout = {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
|
81
test/parallel/test-http2-compat-expect-continue-check.js
Normal file
81
test/parallel/test-http2-compat-expect-continue-check.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Flags: --expose-http2
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const assert = require('assert');
|
||||||
|
const http2 = require('http2');
|
||||||
|
|
||||||
|
const testResBody = 'other stuff!\n';
|
||||||
|
|
||||||
|
// Checks the full 100-continue flow from client sending 'expect: 100-continue'
|
||||||
|
// through server receiving it, triggering 'checkContinue' custom handler,
|
||||||
|
// writing the rest of the request to finally the client receiving to.
|
||||||
|
|
||||||
|
function handler(req, res) {
|
||||||
|
console.error('Server sent full response');
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-type': 'text/plain',
|
||||||
|
'abcd': '1'
|
||||||
|
});
|
||||||
|
res.end(testResBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http2.createServer(
|
||||||
|
common.mustNotCall('Full request received before 100 Continue')
|
||||||
|
);
|
||||||
|
|
||||||
|
server.on('checkContinue', common.mustCall((req, res) => {
|
||||||
|
console.error('Server received Expect: 100-continue');
|
||||||
|
|
||||||
|
res.writeContinue();
|
||||||
|
|
||||||
|
// timeout so that we allow the client to receive continue first
|
||||||
|
setTimeout(
|
||||||
|
common.mustCall(() => handler(req, res)),
|
||||||
|
common.platformTimeout(100)
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.listen(0);
|
||||||
|
|
||||||
|
server.on('listening', common.mustCall(() => {
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
const port = server.address().port;
|
||||||
|
const client = http2.connect(`http://localhost:${port}`);
|
||||||
|
const req = client.request({
|
||||||
|
':method': 'POST',
|
||||||
|
':path': '/world',
|
||||||
|
expect: '100-continue'
|
||||||
|
});
|
||||||
|
console.error('Client sent request');
|
||||||
|
|
||||||
|
let gotContinue = false;
|
||||||
|
req.on('continue', common.mustCall(() => {
|
||||||
|
console.error('Client received 100-continue');
|
||||||
|
gotContinue = true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
req.on('response', common.mustCall((headers) => {
|
||||||
|
console.error('Client received response headers');
|
||||||
|
|
||||||
|
assert.strictEqual(gotContinue, true);
|
||||||
|
assert.strictEqual(headers[':status'], 200);
|
||||||
|
assert.strictEqual(headers['abcd'], '1');
|
||||||
|
}));
|
||||||
|
|
||||||
|
req.setEncoding('utf-8');
|
||||||
|
req.on('data', common.mustCall((chunk) => { body += chunk; }));
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
console.error('Client received full response');
|
||||||
|
|
||||||
|
assert.strictEqual(body, testResBody);
|
||||||
|
|
||||||
|
client.destroy();
|
||||||
|
server.close();
|
||||||
|
}));
|
||||||
|
}));
|
66
test/parallel/test-http2-compat-expect-continue.js
Normal file
66
test/parallel/test-http2-compat-expect-continue.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Flags: --expose-http2
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const assert = require('assert');
|
||||||
|
const http2 = require('http2');
|
||||||
|
|
||||||
|
const testResBody = 'other stuff!\n';
|
||||||
|
|
||||||
|
// Checks the full 100-continue flow from client sending 'expect: 100-continue'
|
||||||
|
// through server receiving it, sending back :status 100, writing the rest of
|
||||||
|
// the request to finally the client receiving to.
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
|
||||||
|
let sentResponse = false;
|
||||||
|
|
||||||
|
server.on('request', common.mustCall((req, res) => {
|
||||||
|
console.error('Server sent full response');
|
||||||
|
|
||||||
|
res.end(testResBody);
|
||||||
|
sentResponse = true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
server.listen(0);
|
||||||
|
|
||||||
|
server.on('listening', common.mustCall(() => {
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
const port = server.address().port;
|
||||||
|
const client = http2.connect(`http://localhost:${port}`);
|
||||||
|
const req = client.request({
|
||||||
|
':method': 'POST',
|
||||||
|
':path': '/world',
|
||||||
|
expect: '100-continue'
|
||||||
|
});
|
||||||
|
console.error('Client sent request');
|
||||||
|
|
||||||
|
let gotContinue = false;
|
||||||
|
req.on('continue', common.mustCall(() => {
|
||||||
|
console.error('Client received 100-continue');
|
||||||
|
gotContinue = true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
req.on('response', common.mustCall((headers) => {
|
||||||
|
console.error('Client received response headers');
|
||||||
|
|
||||||
|
assert.strictEqual(gotContinue, true);
|
||||||
|
assert.strictEqual(sentResponse, true);
|
||||||
|
assert.strictEqual(headers[':status'], 200);
|
||||||
|
}));
|
||||||
|
|
||||||
|
req.setEncoding('utf8');
|
||||||
|
req.on('data', common.mustCall((chunk) => { body += chunk; }));
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
console.error('Client received full response');
|
||||||
|
|
||||||
|
assert.strictEqual(body, testResBody);
|
||||||
|
|
||||||
|
client.destroy();
|
||||||
|
server.close();
|
||||||
|
}));
|
||||||
|
}));
|
Loading…
x
Reference in New Issue
Block a user