Skip to content

Commit 31d3521

Browse files
committed
http2: emit error on canceled streams when aborted event is not emitted
When a client HTTP/2 stream's writable side is already ended (e.g. GET requests), receiving RST code 8 (NGHTTP2_CANCEL) emitted neither the 'aborted' nor the 'error' event, causing the stream to close silently. This happened because the 'aborted' event is only emitted when the writable side is still open, but the NGHTTP2_CANCEL code was unconditionally excluded from error generation assuming the 'aborted' event would cover it. Fix by only skipping error generation for NGHTTP2_CANCEL when the 'aborted' event was actually emitted. Fixes: #56627
1 parent 58a8e1d commit 31d3521

9 files changed

Lines changed: 67 additions & 8 deletions

lib/internal/http2/core.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2470,8 +2470,11 @@ class Http2Stream extends Duplex {
24702470

24712471
// RST code 8 not emitted as an error as its used by clients to signify
24722472
// abort and is already covered by aborted event, also allows more
2473-
// seamless compatibility with http1
2474-
if (err == null && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL)
2473+
// seamless compatibility with http1.
2474+
// However, if the aborted event was not emitted (e.g. because the
2475+
// writable side was already ended), the error must still be reported.
2476+
if (err == null && code !== NGHTTP2_NO_ERROR &&
2477+
!(code === NGHTTP2_CANCEL && this.aborted))
24752478
err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code);
24762479

24772480
this[kSession] = undefined;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const h2 = require('http2');
8+
9+
// Regression test for https://github.com/nodejs/node/issues/56627
10+
// When a client stream's writable side is already ended (e.g. GET request)
11+
// and the server destroys the session, the client stream should emit an
12+
// error event with RST code NGHTTP2_CANCEL, since the 'aborted' event
13+
// cannot be emitted when the writable side is already ended.
14+
15+
{
16+
const server = h2.createServer();
17+
server.on('stream', common.mustCall((stream) => {
18+
stream.session.destroy();
19+
}));
20+
21+
server.listen(0, common.mustCall(() => {
22+
const client = h2.connect(`http://localhost:${server.address().port}`);
23+
24+
client.on('close', common.mustCall(() => {
25+
server.close();
26+
}));
27+
28+
const req = client.request();
29+
30+
req.on('error', common.mustCall((err) => {
31+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
32+
assert.match(err.message, /NGHTTP2_CANCEL/);
33+
}));
34+
35+
req.on('aborted', common.mustNotCall());
36+
37+
req.on('close', common.mustCall(() => {
38+
assert.strictEqual(req.rstCode, h2.constants.NGHTTP2_CANCEL);
39+
}));
40+
41+
req.resume();
42+
}));
43+
}

test/parallel/test-http2-client-destroy.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ const { listenerCount } = require('events');
118118
client.destroy();
119119
});
120120

121-
client.request();
121+
const req = client.request();
122+
req.on('error', common.mustCall((err) => {
123+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
124+
}));
122125
}));
123126
}
124127

test/parallel/test-http2-client-jsstream-destroy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ server.listen(0, common.mustCall(function() {
4545
createConnection: () => proxy
4646
});
4747
const req = client.request();
48+
req.on('error', common.mustCall());
4849

4950
server.on('request', () => {
5051
socket.destroy();

test/parallel/test-http2-client-socket-destroy.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ server.on('stream', common.mustCall((stream) => {
2626
server.listen(0, common.mustCall(function() {
2727
const client = h2.connect(`http://localhost:${this.address().port}`);
2828
const req = client.request();
29+
req.on('error', common.mustCall());
2930

3031
req.on('response', common.mustCall(() => {
3132
// Send a premature socket close
3233
client[kSocket].destroy();
3334
}));
3435

3536
req.resume();
36-
req.on('end', common.mustCall());
37+
req.on('end', common.mustNotCall());
3738
req.on('close', common.mustCall(() => server.close()));
3839

3940
// On the client, the close event must call

test/parallel/test-http2-respond-with-file-connection-abort.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ const {
1313

1414
const server = http2.createServer();
1515
server.on('stream', common.mustCall((stream) => {
16-
stream.on('error', common.mustCallAtLeast((err) => assert.strictEqual(err.code, 'ECONNRESET'), 0));
16+
stream.on('error', common.mustCallAtLeast((err) => {
17+
assert.ok(
18+
err.code === 'ECONNRESET' || err.code === 'ERR_HTTP2_STREAM_ERROR',
19+
`Unexpected error code: ${err.code}`
20+
);
21+
}, 0));
1722
stream.respondWithFile(process.execPath, {
1823
[HTTP2_HEADER_CONTENT_TYPE]: 'application/octet-stream'
1924
});
@@ -22,11 +27,11 @@ server.on('stream', common.mustCall((stream) => {
2227
server.listen(0, common.mustCall(() => {
2328
const client = http2.connect(`http://localhost:${server.address().port}`);
2429
const req = client.request();
30+
req.on('error', common.mustCall());
2531

2632
req.on('response', common.mustCall());
2733
req.once('data', common.mustCall(() => {
2834
net.Socket.prototype.destroy.call(client.socket);
2935
server.close();
3036
}));
31-
req.end();
3237
}));

test/parallel/test-http2-server-shutdown-options-errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ server.listen(
5858
common.mustCall(() => {
5959
const client = http2.connect(`http://localhost:${server.address().port}`);
6060
const req = client.request();
61+
req.on('error', common.mustCall());
6162
req.resume();
6263
req.on('close', common.mustCall(() => {
6364
client.close();

test/parallel/test-http2-server-stream-session-destroy.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ server.on('stream', common.mustCall((stream) => {
5252
server.listen(0, common.mustCall(() => {
5353
const client = h2.connect(`http://localhost:${server.address().port}`);
5454
const req = client.request();
55+
req.on('error', common.mustCall());
5556
req.resume();
56-
req.on('end', common.mustCall());
57+
req.on('end', common.mustNotCall());
5758
req.on('close', common.mustCall(() => server.close(common.mustCall())));
5859
}));

test/parallel/test-http2-zero-length-header.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ server.on('stream', common.mustCall((stream, headers) => {
2222
}));
2323
server.listen(0, common.mustCall(() => {
2424
const client = http2.connect(`http://localhost:${server.address().port}/`);
25-
client.request({ ':path': '/', '': 'foo', 'bar': '' }).end();
25+
const req = client.request({ ':path': '/', '': 'foo', 'bar': '' });
26+
req.on('error', common.mustCall());
2627
}));

0 commit comments

Comments
 (0)