Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
c0eebdafb0 test(http2): add settings-only test case for large payloads
Add a second test to verify that `initialWindowSize` in connect settings
alone (without `setLocalWindowSize()`) also allows receiving payloads
larger than the default 65,535-byte connection window. This works because
`incrementWindowSizeIfNeeded` sends WINDOW_UPDATE frames to replenish
the connection window as data is consumed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-11 22:11:59 +00:00
Claude Bot
b0e2718560 fix(http2): send connection-level WINDOW_UPDATE in setLocalWindowSize
`setLocalWindowSize()` updated the internal connection window size but
never sent a WINDOW_UPDATE frame for stream 0 (the connection level) to
the peer. Per RFC 9113 Section 6.9, the INITIAL_WINDOW_SIZE setting only
applies to stream-level windows; the connection-level window must be
updated explicitly via WINDOW_UPDATE. This caused HTTP/2 streams to
stall after receiving 65,535 bytes (the default connection window).

Closes #26915

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-11 21:57:44 +00:00
2 changed files with 82 additions and 0 deletions

View File

@@ -2864,10 +2864,19 @@ pub const H2FrameParser = struct {
if (this.usedWindowSize > windowSizeValue) {
return globalObject.throwInvalidArguments("Expected windowSize to be greater than usedWindowSize", .{});
}
const oldWindowSize = this.windowSize;
this.windowSize = windowSizeValue;
if (this.localSettings.initialWindowSize < windowSizeValue) {
this.localSettings.initialWindowSize = windowSizeValue;
}
// Send a connection-level WINDOW_UPDATE frame to the peer so it knows
// about the increased window. Per RFC 9113 Section 6.9, the
// INITIAL_WINDOW_SIZE setting only applies to stream-level windows;
// the connection-level window must be updated explicitly.
if (windowSizeValue > oldWindowSize) {
const increment: u31 = @truncate(windowSizeValue - oldWindowSize);
this.sendWindowUpdate(0, UInt31WithReserved.init(increment, false));
}
var it = this.streams.valueIterator();
while (it.next()) |stream| {
if (stream.usedWindowSize > windowSizeValue) {

View File

@@ -0,0 +1,73 @@
import { expect, test } from "bun:test";
import http2 from "node:http2";
// Regression test for https://github.com/oven-sh/bun/issues/26915
// setLocalWindowSize() must send a connection-level WINDOW_UPDATE frame.
// Without this, the peer's connection-level window stays at the default
// 65,535 bytes and streams stall when receiving larger payloads.
function startServer(payloadSize: number): Promise<{ port: number; server: http2.Http2Server }> {
const payload = Buffer.alloc(payloadSize, "x");
const { promise, resolve } = Promise.withResolvers<{ port: number; server: http2.Http2Server }>();
const server = http2.createServer();
server.on("stream", stream => {
stream.respond({ ":status": 200 });
stream.end(payload);
});
server.listen(0, () => {
const addr = server.address();
if (addr && typeof addr === "object") {
resolve({ port: addr.port, server });
}
});
return promise;
}
function doRequest(client: http2.ClientHttp2Session): Promise<Buffer> {
const { promise, resolve, reject } = Promise.withResolvers<Buffer>();
const req = client.request({ ":path": "/" });
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks)));
req.on("error", reject);
req.end();
return promise;
}
test("http2 client setLocalWindowSize sends connection-level WINDOW_UPDATE", async () => {
const payloadSize = 256 * 1024; // 256 KB - well above the 65535 default
const { port, server } = await startServer(payloadSize);
try {
const client = http2.connect(`http://localhost:${port}`, {
settings: { initialWindowSize: 10 * 1024 * 1024 },
});
client.setLocalWindowSize(10 * 1024 * 1024);
const result = await doRequest(client);
expect(result.length).toBe(payloadSize);
client.close();
} finally {
server.close();
}
});
test("http2 client initialWindowSize setting allows large stream payloads", async () => {
const payloadSize = 1024 * 1024; // 1 MB
const { port, server } = await startServer(payloadSize);
try {
const client = http2.connect(`http://localhost:${port}`, {
settings: { initialWindowSize: 10 * 1024 * 1024 },
});
const result = await doRequest(client);
expect(result.length).toBe(payloadSize);
client.close();
} finally {
server.close();
}
});