Commit 4830c1180c3 for nodejs
commit 4830c1180c3113866141d203f7f858423606dcf8
Author: Matteo Collina <hello@matteocollina.com>
Date: Thu Nov 5 22:54:08 2026 +0100
http2: handle errors on compat push response streams
`response.createPushResponse()` wrapped the pushed stream in an
`Http2ServerResponse` without attaching an 'error' listener. Request
streams get one from `Http2ServerRequest`, but pushed streams have no
request, so a peer refusing the push with a non-clean RST_STREAM code
(e.g. REFUSED_STREAM) before the response finished emitted an unhandled
'error' on `response.stream` unless the application listened on it.
Attach `onStreamError` to the pushed stream before invoking the
callback, and make `onStreamError` tolerate the `null` request that
core sets on every stream.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/67053
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js
index 407d84e7fc9..2eedfd39010 100644
--- a/lib/internal/http2/compat.js
+++ b/lib/internal/http2/compat.js
@@ -162,9 +162,9 @@ function onStreamEnd() {
function onStreamError(error) {
// Mirror HTTP/1's IncomingMessage._destroy: forward 'error' to req
- // only when a listener is attached.
+ // only when a listener is attached. Pushed streams have no request.
const request = this[kRequest];
- if (request !== undefined && request.listenerCount('error') > 0)
+ if (request != null && request.listenerCount('error') > 0)
request.emit('error', error);
}
@@ -928,6 +928,10 @@ class Http2ServerResponse extends Stream {
callback(err);
return;
}
+ // Pushed streams have no Http2ServerRequest to install the stream
+ // 'error' handler, so attach it here. Otherwise a peer refusing the
+ // push (e.g. RST_STREAM with REFUSED_STREAM) would crash the process.
+ stream.on('error', onStreamError);
callback(null, new Http2ServerResponse(stream));
});
}
diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse-refused.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse-refused.js
new file mode 100644
index 00000000000..3b909992ef7
--- /dev/null
+++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse-refused.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+// A client refusing a push created through the compat API with
+// RST_STREAM(REFUSED_STREAM) must not crash the server with an unhandled
+// 'error' event on the pushed stream.
+
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer(common.mustCall((req, res) => {
+ res.createPushResponse({ ':path': '/style.css' }, common.mustSucceed((pushRes) => {
+ pushRes.on('close', common.mustCall());
+ pushRes.setHeader('content-type', 'text/css');
+ pushRes.end('body { color: red }');
+ }));
+ res.end('hello');
+}));
+
+server.listen(0, common.mustCall(() => {
+ // A zero initial window keeps the pushed body from completing before the
+ // client refuses it, so the server sees a peer reset with its writable
+ // side still pending.
+ const client = http2.connect(`http://localhost:${server.address().port}`, {
+ settings: { initialWindowSize: 0 },
+ });
+
+ client.on('stream', common.mustCall((pushed) => {
+ pushed.on('error', common.expectsError({
+ code: 'ERR_HTTP2_STREAM_ERROR',
+ }));
+ pushed.close(http2.constants.NGHTTP2_REFUSED_STREAM);
+ // Reopen the window so the main response can complete.
+ client.settings({ initialWindowSize: 65535 });
+ }));
+
+ const req = client.request({ ':path': '/' });
+ req.resume();
+ req.on('close', common.mustCall(() => {
+ assert.strictEqual(req.rstCode, http2.constants.NGHTTP2_NO_ERROR);
+ client.close();
+ server.close();
+ }));
+ req.end();
+}));