From 5c4dd6b414088510aa74597e3972da176304180e Mon Sep 17 00:00:00 2001 From: sangwook Date: Fri, 6 Feb 2026 22:22:51 +0900 Subject: [PATCH 1/2] http2: fix FileHandle leak in respondWithFile Ensure that the file handle is closed if header validation fails in respondWithFile. This prevents ERR_INVALID_STATE errors where a FileHandle object is closed during garbage collection. --- lib/internal/http2/core.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 8b526c001004c5..9f21daeff16bfd 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -2680,6 +2680,8 @@ function processRespondWithFD(self, fd, headers, offset = 0, length = -1, try { headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse); } catch (err) { + if (self.ownsFd) + tryClose(fd); self.destroy(err); return; } @@ -2693,6 +2695,8 @@ function processRespondWithFD(self, fd, headers, offset = 0, length = -1, const ret = self[kHandle].respond(headersList, streamOptions); if (ret < 0) { + if (self.ownsFd) + tryClose(fd); self.destroy(new NghttpError(ret)); return; } From f6d474a05f444e0fcbcb196986d11e0d383c0a3d Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 7 Feb 2026 00:16:44 +0900 Subject: [PATCH 2/2] test: add regression test for http2 fd leak Verifies that the file descriptor is closed when an error occurs during header validation in respondWithFile. Refs: 5c4dd6b414088510aa74597e3972da176304180e --- .../test-http2-respond-file-fd-leak.js | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/parallel/test-http2-respond-file-fd-leak.js diff --git a/test/parallel/test-http2-respond-file-fd-leak.js b/test/parallel/test-http2-respond-file-fd-leak.js new file mode 100644 index 00000000000000..45e12185008a45 --- /dev/null +++ b/test/parallel/test-http2-respond-file-fd-leak.js @@ -0,0 +1,51 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); + +const fname = fixtures.path('elipses.txt'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + const originalClose = fs.close; + let fdClosed = false; + + fs.close = common.mustCall(function(fd, cb) { + fdClosed = true; + return originalClose.apply(this, arguments); + }); + + const headers = { + ':method': 'GET', + 'content-type': 'text/plain' + }; + + stream.respondWithFile(fname, headers); + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_INVALID_PSEUDOHEADER'); + })); + + stream.on('close', common.mustCall(() => { + fs.close = originalClose; + assert.strictEqual(fdClosed, true); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); + + req.on('error', common.mustCall()); + req.end(); +}));