Skip to content

Commit f612a6d

Browse files
apapirovskiMylesBorins
authored andcommitted
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: #15039 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent d176a18 commit f612a6d

File tree

5 files changed

+223
-5
lines changed

5 files changed

+223
-5
lines changed

doc/api/http2.md

+60-2
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,15 @@ used exclusively on HTTP/2 Clients. `Http2Stream` instances on the client
849849
provide events such as `'response'` and `'push'` that are only relevant on
850850
the client.
851851

852+
#### Event: 'continue'
853+
<!-- YAML
854+
added: REPLACEME
855+
-->
856+
857+
Emitted when the server sends a `100 Continue` status, usually because
858+
the request contained `Expect: 100-continue`. This is an instruction that
859+
the client should send the request body.
860+
852861
#### Event: 'headers'
853862
<!-- YAML
854863
added: v8.4.0
@@ -1306,6 +1315,28 @@ added: v8.4.0
13061315
The `'timeout'` event is emitted when there is no activity on the Server for
13071316
a given number of milliseconds set using `http2server.setTimeout()`.
13081317

1318+
#### Event: 'checkContinue'
1319+
<!-- YAML
1320+
added: REPLACEME
1321+
-->
1322+
1323+
* `request` {http2.Http2ServerRequest}
1324+
* `response` {http2.Http2ServerResponse}
1325+
1326+
If a [`'request'`][] listener is registered or [`'http2.createServer()'`][] is
1327+
supplied a callback function, the `'checkContinue'` event is emitted each time
1328+
a request with an HTTP `Expect: 100-continue` is received. If this event is
1329+
not listened for, the server will automatically respond with a status
1330+
`100 Continue` as appropriate.
1331+
1332+
Handling this event involves calling [`response.writeContinue()`][] if the client
1333+
should continue to send the request body, or generating an appropriate HTTP
1334+
response (e.g. 400 Bad Request) if the client should not continue to send the
1335+
request body.
1336+
1337+
Note that when this event is emitted and handled, the [`'request'`][] event will
1338+
not be emitted.
1339+
13091340
### Class: Http2SecureServer
13101341
<!-- YAML
13111342
added: v8.4.0
@@ -1389,6 +1420,28 @@ per session. See the [Compatibility API](compatiblity-api).
13891420
added: v8.4.0
13901421
-->
13911422

1423+
#### Event: 'checkContinue'
1424+
<!-- YAML
1425+
added: REPLACEME
1426+
-->
1427+
1428+
* `request` {http2.Http2ServerRequest}
1429+
* `response` {http2.Http2ServerResponse}
1430+
1431+
If a [`'request'`][] listener is registered or [`'http2.createSecureServer()'`][]
1432+
is supplied a callback function, the `'checkContinue'` event is emitted each
1433+
time a request with an HTTP `Expect: 100-continue` is received. If this event
1434+
is not listened for, the server will automatically respond with a status
1435+
`100 Continue` as appropriate.
1436+
1437+
Handling this event involves calling [`response.writeContinue()`][] if the client
1438+
should continue to send the request body, or generating an appropriate HTTP
1439+
response (e.g. 400 Bad Request) if the client should not continue to send the
1440+
request body.
1441+
1442+
Note that when this event is emitted and handled, the [`'request'`][] event will
1443+
not be emitted.
1444+
13921445
### http2.createServer(options[, onRequestHandler])
13931446
<!-- YAML
13941447
added: v8.4.0
@@ -2537,8 +2590,9 @@ buffer. Returns `false` if all or part of the data was queued in user memory.
25372590
added: v8.4.0
25382591
-->
25392592

2540-
Throws an error as the `'continue'` flow is not current implemented. Added for
2541-
parity with [HTTP/1]().
2593+
Sends a status `100 Continue` to the client, indicating that the request body
2594+
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
2595+
`Http2SecureServer`.
25422596

25432597
### response.writeHead(statusCode[, statusMessage][, headers])
25442598
<!-- YAML
@@ -2618,6 +2672,7 @@ if the stream is closed.
26182672
[Settings Object]: #http2_settings_object
26192673
[Using options.selectPadding]: #http2_using_options_selectpadding
26202674
[Writable Stream]: stream.html#stream_writable_streams
2675+
[`'checkContinue'`]: #http2_event_checkcontinue
26212676
[`'request'`]: #http2_event_request
26222677
[`'unknownProtocol'`]: #http2_event_unknownprotocol
26232678
[`ClientHttp2Stream`]: #http2_class_clienthttp2stream
@@ -2628,14 +2683,17 @@ if the stream is closed.
26282683
[`ServerRequest`]: #http2_class_server_request
26292684
[`TypeError`]: errors.html#errors_class_typeerror
26302685
[`http2.SecureServer`]: #http2_class_http2secureserver
2686+
[`http2.createSecureServer()`]: #http2_createsecureserver_options_onrequesthandler
26312687
[`http2.Server`]: #http2_class_http2server
2688+
[`http2.createServer()`]: #http2_createserver_options_onrequesthandler
26322689
[`net.Socket`]: net.html#net_class_net_socket
26332690
[`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed
26342691
[`response.end()`]: #http2_response_end_data_encoding_callback
26352692
[`response.setHeader()`]: #http2_response_setheader_name_value
26362693
[`response.socket`]: #http2_response_socket
26372694
[`response.write()`]: #http2_response_write_chunk_encoding_callback
26382695
[`response.write(data, encoding)`]: http.html#http_response_write_chunk_encoding_callback
2696+
[`response.writeContinue()`]: #http2_response_writecontinue
26392697
[`response.writeHead()`]: #http2_response_writehead_statuscode_statusmessage_headers
26402698
[`stream.pushStream()`]: #http2_stream-pushstream
26412699
[`tls.TLSSocket`]: tls.html#tls_class_tls_tlssocket

lib/internal/http2/compat.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -529,9 +529,14 @@ class Http2ServerResponse extends Stream {
529529
this.emit('finish');
530530
}
531531

532+
// TODO doesn't support callbacks
532533
writeContinue() {
533-
// TODO mcollina check what is the continue flow
534-
throw new Error('not implemented yet');
534+
const stream = this[kStream];
535+
if (stream === undefined) return false;
536+
this[kStream].additionalHeaders({
537+
[constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_CONTINUE
538+
});
539+
return true;
535540
}
536541
}
537542

@@ -556,7 +561,7 @@ function onServerStream(stream, headers, flags) {
556561
if (server.listenerCount('checkContinue')) {
557562
server.emit('checkContinue', request, response);
558563
} else {
559-
response.sendContinue();
564+
response.writeContinue();
560565
server.emit('request', request, response);
561566
}
562567
} else if (server.listenerCount('checkExpectation')) {

lib/internal/http2/core.js

+8
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const {
120120
HTTP2_METHOD_HEAD,
121121
HTTP2_METHOD_CONNECT,
122122

123+
HTTP_STATUS_CONTINUE,
123124
HTTP_STATUS_CONTENT_RESET,
124125
HTTP_STATUS_OK,
125126
HTTP_STATUS_NO_CONTENT,
@@ -2113,10 +2114,17 @@ class ClientHttp2Stream extends Http2Stream {
21132114
this[kState].headersSent = true;
21142115
if (id !== undefined)
21152116
this[kInit](id);
2117+
this.on('headers', handleHeaderContinue);
21162118
debug(`[${sessionName(session[kType])}] clienthttp2stream created`);
21172119
}
21182120
}
21192121

2122+
function handleHeaderContinue(headers) {
2123+
if (headers[HTTP2_HEADER_STATUS] === HTTP_STATUS_CONTINUE) {
2124+
this.emit('continue');
2125+
}
2126+
}
2127+
21202128
const setTimeout = {
21212129
configurable: true,
21222130
enumerable: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Flags: --expose-http2
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
const assert = require('assert');
8+
const http2 = require('http2');
9+
10+
const testResBody = 'other stuff!\n';
11+
12+
// Checks the full 100-continue flow from client sending 'expect: 100-continue'
13+
// through server receiving it, triggering 'checkContinue' custom handler,
14+
// writing the rest of the request to finally the client receiving to.
15+
16+
function handler(req, res) {
17+
console.error('Server sent full response');
18+
19+
res.writeHead(200, {
20+
'content-type': 'text/plain',
21+
'abcd': '1'
22+
});
23+
res.end(testResBody);
24+
}
25+
26+
const server = http2.createServer(
27+
common.mustNotCall('Full request received before 100 Continue')
28+
);
29+
30+
server.on('checkContinue', common.mustCall((req, res) => {
31+
console.error('Server received Expect: 100-continue');
32+
33+
res.writeContinue();
34+
35+
// timeout so that we allow the client to receive continue first
36+
setTimeout(
37+
common.mustCall(() => handler(req, res)),
38+
common.platformTimeout(100)
39+
);
40+
}));
41+
42+
server.listen(0);
43+
44+
server.on('listening', common.mustCall(() => {
45+
let body = '';
46+
47+
const port = server.address().port;
48+
const client = http2.connect(`http://localhost:${port}`);
49+
const req = client.request({
50+
':method': 'POST',
51+
':path': '/world',
52+
expect: '100-continue'
53+
});
54+
console.error('Client sent request');
55+
56+
let gotContinue = false;
57+
req.on('continue', common.mustCall(() => {
58+
console.error('Client received 100-continue');
59+
gotContinue = true;
60+
}));
61+
62+
req.on('response', common.mustCall((headers) => {
63+
console.error('Client received response headers');
64+
65+
assert.strictEqual(gotContinue, true);
66+
assert.strictEqual(headers[':status'], 200);
67+
assert.strictEqual(headers['abcd'], '1');
68+
}));
69+
70+
req.setEncoding('utf-8');
71+
req.on('data', common.mustCall((chunk) => { body += chunk; }));
72+
73+
req.on('end', common.mustCall(() => {
74+
console.error('Client received full response');
75+
76+
assert.strictEqual(body, testResBody);
77+
78+
client.destroy();
79+
server.close();
80+
}));
81+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Flags: --expose-http2
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
const assert = require('assert');
8+
const http2 = require('http2');
9+
10+
const testResBody = 'other stuff!\n';
11+
12+
// Checks the full 100-continue flow from client sending 'expect: 100-continue'
13+
// through server receiving it, sending back :status 100, writing the rest of
14+
// the request to finally the client receiving to.
15+
16+
const server = http2.createServer();
17+
18+
let sentResponse = false;
19+
20+
server.on('request', common.mustCall((req, res) => {
21+
console.error('Server sent full response');
22+
23+
res.end(testResBody);
24+
sentResponse = true;
25+
}));
26+
27+
server.listen(0);
28+
29+
server.on('listening', common.mustCall(() => {
30+
let body = '';
31+
32+
const port = server.address().port;
33+
const client = http2.connect(`http://localhost:${port}`);
34+
const req = client.request({
35+
':method': 'POST',
36+
':path': '/world',
37+
expect: '100-continue'
38+
});
39+
console.error('Client sent request');
40+
41+
let gotContinue = false;
42+
req.on('continue', common.mustCall(() => {
43+
console.error('Client received 100-continue');
44+
gotContinue = true;
45+
}));
46+
47+
req.on('response', common.mustCall((headers) => {
48+
console.error('Client received response headers');
49+
50+
assert.strictEqual(gotContinue, true);
51+
assert.strictEqual(sentResponse, true);
52+
assert.strictEqual(headers[':status'], 200);
53+
}));
54+
55+
req.setEncoding('utf8');
56+
req.on('data', common.mustCall((chunk) => { body += chunk; }));
57+
58+
req.on('end', common.mustCall(() => {
59+
console.error('Client received full response');
60+
61+
assert.strictEqual(body, testResBody);
62+
63+
client.destroy();
64+
server.close();
65+
}));
66+
}));

0 commit comments

Comments
 (0)