diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index ac480fda35..272f5b446f 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -3466,6 +3466,22 @@ pub const H2FrameParser = struct { const stream_id_arg = args_list.ptr[0]; bun.debugAssert(stream_id_arg.isNumber()); this.lastStreamID = stream_id_arg.to(u32); + // to set the next stream id we need to decrement because we only keep the last stream id + if (this.isServer) { + if (this.lastStreamID % 2 == 0) { + this.lastStreamID -= 2; + } else { + this.lastStreamID -= 1; + } + } else { + if (this.lastStreamID % 2 == 0) { + this.lastStreamID -= 1; + } else if (this.lastStreamID == 1) { + this.lastStreamID = 0; + } else { + this.lastStreamID -= 2; + } + } return .undefined; } @@ -3477,6 +3493,9 @@ pub const H2FrameParser = struct { JSC.markBinding(@src()); const id = this.getNextStreamID(); + if (id > MAX_STREAM_ID) { + return JSC.JSValue.jsNumber(-1); + } _ = this.handleReceivedStreamID(id) orelse { return JSC.JSValue.jsNumber(-1); }; diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index 78a48005a9..08445cfe04 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -130,6 +130,10 @@ function emitErrorNT(self: any, error: any, destroy: boolean) { self.emit("error", error); } } + +function emitOutofStreamErrorNT(self: any) { + self.destroy($ERR_HTTP2_OUT_OF_STREAMS()); +} function cache() { const d = new Date(); utcCache = d.toUTCString(); @@ -1843,7 +1847,7 @@ class Http2Stream extends Duplex { // will destroy if it has been closed and there are no other open or // pending streams. Delay with setImmediate so we don't do it on the // nghttp2 stack. - if (session) { + if (session && typeof this.#id === "number") { setImmediate(rstNextTick.bind(session, this.#id, rstCode)); } callback(err); @@ -3512,13 +3516,13 @@ class ClientHttp2Session extends Http2Session { } } let stream_id: number = this.#parser.getNextStream(); + if (stream_id < 0) { + const req = new ClientHttp2Stream(undefined, this, headers); + process.nextTick(emitOutofStreamErrorNT, req); + return req; + } const req = new ClientHttp2Stream(stream_id, this, headers); req.authority = authority; - if (stream_id < 0) { - const error = $ERR_HTTP2_OUT_OF_STREAMS(); - this.emit("error", error); - return null; - } req[kHeadRequest] = method === HTTP2_METHOD_HEAD; if (typeof options === "undefined") { this.#parser.request(stream_id, req, headers, sensitiveNames); diff --git a/test/js/node/test/parallel/test-http2-no-more-streams.js b/test/js/node/test/parallel/test-http2-no-more-streams.js new file mode 100644 index 0000000000..584447c527 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-no-more-streams.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); +const Countdown = require('../common/countdown'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond(); + stream.end('ok'); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://127.0.0.1:${server.address().port}`); + const nextID = 2 ** 31 - 1; + + client.on('connect', () => { + client.setNextStreamID(nextID); + assert.strictEqual(client.state.nextStreamID, nextID); + + const countdown = new Countdown(2, () => { + server.close(); + client.close(); + }); + + { + // This one will be ok + const req = client.request(); + assert.strictEqual(req.id, nextID); + + req.on('error', common.mustNotCall()); + req.resume(); + req.on('end', () => countdown.dec()); + } + + { + // This one will error because there are no more stream IDs available + const req = client.request(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_OUT_OF_STREAMS', + name: 'Error', + message: + 'No stream ID is available because maximum stream ID has been reached' + })); + req.on('error', () => countdown.dec()); + } + }); +}));