Skip to content

Commit 32a0fb2

Browse files
committed
http2: do not end compat requests reset before END_STREAM
When the peer reset a stream before sending END_STREAM, the compat `Http2ServerRequest` still pushed EOF from its 'close' handler, so it emitted 'end' for a truncated body (on main even after 'error' and 'close'). Unlike http1, where an aborted `IncomingMessage` is destroyed and never emits 'end', applications listening for 'end' then processed the partial body as if it were complete. Record in `onStreamClose()` that the peer closed the stream before END_STREAM, since `destroy()` ends the readable side afterwards, and destroy the request instead of ending it in that case. Requests whose body was complete before the reset still end normally. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 98dbf60 commit 32a0fb2

4 files changed

Lines changed: 102 additions & 3 deletions

File tree

‎lib/internal/http2/compat.js‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const {
5757
kSocket,
5858
kRequest,
5959
kProxySocket,
60+
kIncomplete,
6061
assertValidPseudoHeader,
6162
getAuthority,
6263
} = require('internal/http2/util');
@@ -291,15 +292,24 @@ function onStreamCloseRequest() {
291292
const state = req[kState];
292293
state.closed = true;
293294

295+
this[kProxySocket] = null;
296+
this[kRequest] = undefined;
297+
298+
// The peer closed the stream before END_STREAM, so the request body is
299+
// incomplete. Like http1's IncomingMessage on abort, destroy the request
300+
// instead of ending it, so 'end' is not emitted for a truncated body.
301+
// destroy() emits 'close'.
302+
if (this[kIncomplete]) {
303+
req.destroy();
304+
return;
305+
}
306+
294307
req.push(null);
295308
// If the user didn't interact with incoming data and didn't pipe it,
296309
// dump it for compatibility with http1
297310
if (!state.didRead && !req._readableState.resumeScheduled)
298311
req.resume();
299312

300-
this[kProxySocket] = null;
301-
this[kRequest] = undefined;
302-
303313
req.emit('close');
304314
}
305315

‎lib/internal/http2/core.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ const {
156156
kStrictSingleValueFields,
157157
kSocket,
158158
kRequest,
159+
kIncomplete,
159160
kProtocol,
160161
kProxySocket,
161162
MAX_ADDITIONAL_SETTINGS,
@@ -652,6 +653,9 @@ function onStreamClose(code, peerReset) {
652653
const cleanCode = code === NGHTTP2_NO_ERROR || code === NGHTTP2_CANCEL;
653654

654655
if (!peerReadableEnded) {
656+
// The peer closed the stream before END_STREAM, so the incoming message
657+
// is incomplete. destroy() ends the readable side, so record it here.
658+
stream[kIncomplete] = true;
655659
stream.destroy(cleanCode ?
656660
new ERR_HTTP2_STREAM_ABORTED() :
657661
new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code));

‎lib/internal/http2/util.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const kSocket = Symbol('socket');
4646
const kProtocol = Symbol('protocol');
4747
const kProxySocket = Symbol('proxySocket');
4848
const kRequest = Symbol('request');
49+
const kIncomplete = Symbol('incomplete');
4950

5051
const {
5152
NGHTTP2_NV_FLAG_NONE,
@@ -1005,6 +1006,7 @@ module.exports = {
10051006
kStrictSingleValueFields,
10061007
kSocket,
10071008
kProtocol,
1009+
kIncomplete,
10081010
kProxySocket,
10091011
kRequest,
10101012
MAX_ADDITIONAL_SETTINGS,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
// A request whose stream is reset by the peer before END_STREAM has an
8+
// incomplete body. Like http1, the compat request must not emit 'end' for it.
9+
10+
const assert = require('assert');
11+
const http2 = require('http2');
12+
const { finished } = require('stream');
13+
14+
const codes = [
15+
http2.constants.NGHTTP2_NO_ERROR,
16+
http2.constants.NGHTTP2_CANCEL,
17+
];
18+
19+
const server = http2.createServer(common.mustCall((req, res) => {
20+
// Tell the client the partial body arrived, so it can reset the stream.
21+
req.once('data', common.mustCall(() => res.writeHead(200).flushHeaders()));
22+
req.on('end', common.mustNotCall());
23+
req.on('aborted', common.mustCall());
24+
req.on('error', common.expectsError({ code: 'ERR_HTTP2_STREAM_ABORTED' }));
25+
req.on('close', common.mustCall(() => {
26+
assert.strictEqual(req.aborted, true);
27+
assert.strictEqual(req.readableEnded, false);
28+
}));
29+
finished(req, common.mustCall((err) => {
30+
assert.ok(err);
31+
}));
32+
}, codes.length));
33+
34+
server.listen(0, common.mustCall(() => {
35+
const client = http2.connect(`http://localhost:${server.address().port}`);
36+
let pending = codes.length;
37+
38+
for (const code of codes) {
39+
const req = client.request({
40+
':method': 'POST',
41+
'content-length': '1000',
42+
});
43+
req.on('error', () => {});
44+
req.on('close', common.mustCall(() => {
45+
if (--pending === 0) {
46+
client.close();
47+
server.close();
48+
}
49+
}));
50+
// Send part of the body, then reset the stream without END_STREAM once
51+
// the server has seen it.
52+
req.write('{"action":"partial"}');
53+
req.on('response', common.mustCall(() => req.close(code)));
54+
}
55+
}));
56+
57+
// A complete body followed by a reset still ends the request normally.
58+
{
59+
const server = http2.createServer(common.mustCall((req, res) => {
60+
let body = '';
61+
req.setEncoding('utf8');
62+
req.on('data', (chunk) => body += chunk);
63+
req.on('end', common.mustCall(() => {
64+
assert.strictEqual(body, 'complete');
65+
res.writeHead(200).flushHeaders();
66+
}));
67+
req.on('error', common.mustNotCall());
68+
}));
69+
70+
server.listen(0, common.mustCall(() => {
71+
const client = http2.connect(`http://localhost:${server.address().port}`);
72+
const req = client.request({ ':method': 'POST' });
73+
req.on('error', () => {});
74+
req.on('response', common.mustCall(() => {
75+
req.close(http2.constants.NGHTTP2_CANCEL);
76+
}));
77+
req.on('close', common.mustCall(() => {
78+
client.close();
79+
server.close();
80+
}));
81+
req.end('complete');
82+
}));
83+
}

0 commit comments

Comments
 (0)