Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2409,8 +2409,20 @@ class Http2Stream extends Duplex {
validateFunction(callback, 'callback');
}

if (this.closed)
if (this.closed) {
// A client stream may already have been marked as closed with
// NGHTTP2_NO_ERROR by the time the session or underlying socket is
// canceled. Preserve the cancelation code so _destroy() can still emit
// the expected stream error when the aborted event was not emitted.
if (code === NGHTTP2_CANCEL &&
this[kSession] !== undefined &&
this[kSession][kType] === NGHTTP2_SESSION_CLIENT &&
this.rstCode === NGHTTP2_NO_ERROR &&
!this.aborted) {
this[kState].rstCode = code;
}
return;
}

if (callback !== undefined)
this.once('close', callback);
Expand Down Expand Up @@ -2468,11 +2480,17 @@ class Http2Stream extends Duplex {
sessionState.writeQueueSize -= state.writeQueueSize;
state.writeQueueSize = 0;

// RST code 8 not emitted as an error as its used by clients to signify
// abort and is already covered by aborted event, also allows more
// seamless compatibility with http1
if (err == null && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL)
// RST code 8 is commonly used by clients to signify abort and is already
// covered by the aborted event, which also keeps better compatibility with
// http1.
// However, if the aborted event was not emitted (e.g. because the
// writable side was already ended), client streams must still report the
// cancelation as an error.
if (err == null && code !== NGHTTP2_NO_ERROR &&
(code !== NGHTTP2_CANCEL ||
session[kType] === NGHTTP2_SESSION_CLIENT && !this.aborted)) {
err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code);
}

this[kSession] = undefined;
this[kHandle] = undefined;
Expand Down
43 changes: 43 additions & 0 deletions test/parallel/test-http2-client-cancel-stream-after-end.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');

// Regression test for https://github.com/nodejs/node/issues/56627
// When a client stream's writable side is already ended (e.g. GET request)
// and the server destroys the session, the client stream should emit an
// error event with RST code NGHTTP2_CANCEL, since the 'aborted' event
// cannot be emitted when the writable side is already ended.

{
const server = h2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.session.destroy();
}));

server.listen(0, common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`);

client.on('close', common.mustCall(() => {
server.close();
}));

const req = client.request();

req.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
assert.match(err.message, /NGHTTP2_CANCEL/);
}));

req.on('aborted', common.mustNotCall());

req.on('close', common.mustCall(() => {
assert.strictEqual(req.rstCode, h2.constants.NGHTTP2_CANCEL);

Check failure on line 38 in test/parallel/test-http2-client-cancel-stream-after-end.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 0 !== 8 at ClientHttp2Stream.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-http2-client-cancel-stream-after-end.js:38:14) at ClientHttp2Stream.<anonymous> (/Users/runner/work/node/node/node/test/common/index.js:508:15) at ClientHttp2Stream.emit (node:events:509:20) at emitCloseNT (node:internal/streams/destroy:148:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 0, expected: 8, operator: 'strictEqual', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /Users/runner/work/node/node/node/test/parallel/test-http2-client-cancel-stream-after-end.js

Check failure on line 38 in test/parallel/test-http2-client-cancel-stream-after-end.js

View workflow job for this annotation

GitHub Actions / x86_64-darwin: with shared libraries

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 0 !== 8 at ClientHttp2Stream.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-cancel-stream-after-end.js:38:14) at ClientHttp2Stream.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at ClientHttp2Stream.emit (node:events:509:20) at emitCloseNT (node:internal/streams/destroy:148:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 0, expected: 8, operator: 'strictEqual', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-cancel-stream-after-end.js

Check failure on line 38 in test/parallel/test-http2-client-cancel-stream-after-end.js

View workflow job for this annotation

GitHub Actions / aarch64-darwin: with shared libraries

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 0 !== 8 at ClientHttp2Stream.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-cancel-stream-after-end.js:38:14) at ClientHttp2Stream.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at ClientHttp2Stream.emit (node:events:509:20) at emitCloseNT (node:internal/streams/destroy:148:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) { generatedMessage: true, code: 'ERR_ASSERTION', actual: 0, expected: 8, operator: 'strictEqual', diff: 'simple' } Node.js v26.0.0-pre Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-cancel-stream-after-end.js
}));

req.resume();
}));
}
5 changes: 4 additions & 1 deletion test/parallel/test-http2-client-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@
client.destroy();
});

client.request();
const req = client.request();
req.on('error', common.mustCall((err) => {

Check failure on line 122 in test/parallel/test-http2-client-destroy.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stdout --- Mismatched <anonymous> function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/node/node/node/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-http2-client-destroy.js:122:28) at Http2Server.<anonymous> (/Users/runner/work/node/node/node/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node --expose-internals /Users/runner/work/node/node/node/test/parallel/test-http2-client-destroy.js

Check failure on line 122 in test/parallel/test-http2-client-destroy.js

View workflow job for this annotation

GitHub Actions / x86_64-darwin: with shared libraries

--- stdout --- Mismatched <anonymous> function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-destroy.js:122:28) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node --expose-internals /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-destroy.js

Check failure on line 122 in test/parallel/test-http2-client-destroy.js

View workflow job for this annotation

GitHub Actions / aarch64-darwin: with shared libraries

--- stdout --- Mismatched <anonymous> function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-destroy.js:122:28) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node --expose-internals /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-client-destroy.js
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
}));
}));
}

Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-http2-client-jsstream-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ server.listen(0, common.mustCall(function() {
createConnection: () => proxy
});
const req = client.request();
req.on('error', common.mustCall());

server.on('request', () => {
socket.destroy();
Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-http2-client-socket-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ server.on('stream', common.mustCall((stream) => {
server.listen(0, common.mustCall(function() {
const client = h2.connect(`http://localhost:${this.address().port}`);
const req = client.request();
req.on('error', common.mustCall());

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

req.resume();
req.on('end', common.mustCall());
req.on('end', common.mustNotCall());
req.on('close', common.mustCall(() => server.close()));

// On the client, the close event must call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ const {

const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.on('error', common.mustCallAtLeast((err) => assert.strictEqual(err.code, 'ECONNRESET'), 0));
stream.on('error', common.mustCallAtLeast((err) => {
assert.ok(
err.code === 'ECONNRESET' || err.code === 'ERR_HTTP2_STREAM_ERROR',
`Unexpected error code: ${err.code}`
);
}, 0));
stream.respondWithFile(process.execPath, {
[HTTP2_HEADER_CONTENT_TYPE]: 'application/octet-stream'
});
Expand All @@ -22,11 +27,11 @@ server.on('stream', common.mustCall((stream) => {
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('error', common.mustCall());

req.on('response', common.mustCall());
req.once('data', common.mustCall(() => {
net.Socket.prototype.destroy.call(client.socket);
server.close();
}));
req.end();
}));
1 change: 1 addition & 0 deletions test/parallel/test-http2-server-shutdown-options-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('error', common.mustCall());

Check failure on line 61 in test/parallel/test-http2-server-shutdown-options-errors.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stdout --- Mismatched noop function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/node/node/node/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-http2-server-shutdown-options-errors.js:61:28) at Http2Server.<anonymous> (/Users/runner/work/node/node/node/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node /Users/runner/work/node/node/node/test/parallel/test-http2-server-shutdown-options-errors.js

Check failure on line 61 in test/parallel/test-http2-server-shutdown-options-errors.js

View workflow job for this annotation

GitHub Actions / x86_64-darwin: with shared libraries

--- stdout --- Mismatched noop function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-server-shutdown-options-errors.js:61:28) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-server-shutdown-options-errors.js

Check failure on line 61 in test/parallel/test-http2-server-shutdown-options-errors.js

View workflow job for this annotation

GitHub Actions / aarch64-darwin: with shared libraries

--- stdout --- Mismatched noop function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-server-shutdown-options-errors.js:61:28) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-server-shutdown-options-errors.js
req.resume();
req.on('close', common.mustCall(() => {
client.close();
Expand Down
3 changes: 2 additions & 1 deletion test/parallel/test-http2-server-stream-session-destroy.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'use strict';

const common = require('../common');
Expand Down Expand Up @@ -52,7 +52,8 @@
server.listen(0, common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('error', common.mustCall());
req.resume();
req.on('end', common.mustCall());
req.on('end', common.mustNotCall());
req.on('close', common.mustCall(() => server.close(common.mustCall())));
}));
3 changes: 2 additions & 1 deletion test/parallel/test-http2-zero-length-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}/`);
client.request({ ':path': '/', '': 'foo', 'bar': '' }).end();
const req = client.request({ ':path': '/', '': 'foo', 'bar': '' });
req.on('error', common.mustCall());

Check failure on line 26 in test/parallel/test-http2-zero-length-header.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stdout --- Mismatched noop function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/node/node/node/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-http2-zero-length-header.js:26:26) at Http2Server.<anonymous> (/Users/runner/work/node/node/node/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node /Users/runner/work/node/node/node/test/parallel/test-http2-zero-length-header.js

Check failure on line 26 in test/parallel/test-http2-zero-length-header.js

View workflow job for this annotation

GitHub Actions / x86_64-darwin: with shared libraries

--- stdout --- Mismatched noop function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-zero-length-header.js:26:26) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-zero-length-header.js

Check failure on line 26 in test/parallel/test-http2-zero-length-header.js

View workflow job for this annotation

GitHub Actions / aarch64-darwin: with shared libraries

--- stdout --- Mismatched noop function calls. Expected exactly 1, actual 0. at Proxy.mustCall (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:466:10) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-zero-length-header.js:26:26) at Http2Server.<anonymous> (/Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/common/index.js:508:15) at Object.onceWrapper (node:events:630:28) at Http2Server.emit (node:events:509:20) at emitListeningNT (node:net:2051:10) at process.processTicksAndRejections (node:internal/process/task_queues:89:21) Command: out/Release/node /Users/runner/work/_temp/node-v26.0.0-nightly2026-04-25b3a8d69bf9-slim/test/parallel/test-http2-zero-length-header.js
}));
Loading