Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
http2: fix stream error reporting & 'finish' in the compat API
Previously, stream errors were completely swallowed and never exposed.
With this change, they're exposed only if there is a listener for them -
this is the exact same pattern used in HTTP/1 itself, so hopefully a
good fit for the compat API!

This also changes the compat API to only report 'finish' when the
writable has actually finished - previously all closes were reporting
with finish, even when the writable was aborted part way through.
  • Loading branch information
pimterry committed May 11, 2026
commit 69322256974a245a0aaef3a5a5c253648af3ed74
14 changes: 8 additions & 6 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ function onStreamEnd() {
}

function onStreamError(error) {
// This is purposefully left blank
//
// errors in compatibility mode are
// not forwarded to the request
// and response objects.
// Mirror HTTP/1's IncomingMessage._destroy: forward 'error' to req
// only when a listener is attached.
const request = this[kRequest];
if (request !== undefined && request.listenerCount('error') > 0)
request.emit('error', error);
}

function onRequestPause() {
Expand Down Expand Up @@ -460,7 +460,9 @@ function onStreamCloseResponse() {
this.removeListener('wantTrailers', onStreamTrailersReady);
this[kResponse] = undefined;

res.emit('finish');
// Only emit 'finish' when the underlying writable actually finished
if (this.writableFinished)
res.emit('finish');
res.emit('close');
}

Expand Down
12 changes: 8 additions & 4 deletions test/parallel/test-http2-compat-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');

// Errors should not be reported both in Http2ServerRequest
// and Http2ServerResponse
// Errors on the underlying stream surface on Http2ServerRequest

let expected = null;

const server = h2.createServer(common.mustCall(function(req, res) {
res.stream.on('error', common.mustCall());
req.on('error', common.mustNotCall());
res.stream.on('error', common.mustCall((err) => {
assert.strictEqual(err, expected);
}));
req.on('error', common.mustCall((err) => {
assert.strictEqual(err, expected);
}));
res.on('error', common.mustNotCall());
req.on('aborted', common.mustCall());
res.on('aborted', common.mustNotCall());
Expand Down
7 changes: 5 additions & 2 deletions test/parallel/test-http2-compat-serverresponse-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ const errors = [
let nextError;

const server = http2.createServer(common.mustCall((req, res) => {
req.on('error', common.mustNotCall());
if (req.url !== '/')
req.on('error', common.mustCall());
else
req.on('error', common.mustNotCall());
res.on('error', common.mustNotCall());

res.on('finish', common.mustCall(() => {
res.on('close', common.mustCall(() => {
res.destroy(nextError);
process.nextTick(() => {
res.destroy(nextError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
response.on('finish', common.mustCall(() => {
response.on('close', common.mustCall(() => {
assert.strictEqual(response.headersSent, false);
response.setHeader('test', 'value');
response.removeHeader('test', 'value');
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-http2-compat-socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ server.on('request', common.mustCall(function(request, response) {
assert.strictEqual(request.socket.readable, false);
response.socket.destroy();
}));
response.on('finish', common.mustCall(() => {
response.on('close', common.mustCall(() => {
assert.ok(request.socket);
assert.strictEqual(response.socket, undefined);
assert.ok(request.socket.destroyed);
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-http2-many-writes-and-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const http2 = require('http2');

{
const server = http2.createServer((req, res) => {
// Peer destroys mid-stream, so we see errors here:
req.on('error', () => {});
res.on('error', () => {});
req.pipe(res);
});

Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-http2-server-rst-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const {
// (NO_ERROR/CANCEL) yield ERR_HTTP2_STREAM_ABORTED, other codes yield
// ERR_HTTP2_STREAM_ERROR.
const tests = [
[NGHTTP2_NO_ERROR, 'ERR_HTTP2_STREAM_ABORTED'],
[NGHTTP2_NO_ERROR, 'ERR_HTTP2_STREAM_ABORTED'],
[NGHTTP2_PROTOCOL_ERROR, 'ERR_HTTP2_STREAM_ERROR'],
[NGHTTP2_CANCEL, 'ERR_HTTP2_STREAM_ABORTED'],
Expand Down
Loading