mirror of
https://github.com/oven-sh/bun
synced 2026-02-18 23:01:58 +00:00
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>
This commit is contained in:
@@ -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) {
|
||||
|
||||
58
test/regression/issue/26915.test.ts
Normal file
58
test/regression/issue/26915.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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.
|
||||
test("http2 client setLocalWindowSize sends connection-level WINDOW_UPDATE", async () => {
|
||||
const payloadSize = 256 * 1024; // 256 KB - well above the 65535 default
|
||||
const payload = Buffer.alloc(payloadSize, "x");
|
||||
|
||||
const { promise: serverReady, resolve: resolveServer } = 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") {
|
||||
resolveServer({ port: addr.port, server });
|
||||
}
|
||||
});
|
||||
|
||||
const { port, server: srv } = await serverReady;
|
||||
|
||||
try {
|
||||
const client = http2.connect(`http://localhost:${port}`, {
|
||||
settings: { initialWindowSize: 10 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
client.setLocalWindowSize(10 * 1024 * 1024);
|
||||
|
||||
const { promise: done, resolve: resolveDone, reject: rejectDone } = Promise.withResolvers<Buffer>();
|
||||
|
||||
const req = client.request({ ":path": "/" });
|
||||
const chunks: Buffer[] = [];
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
resolveDone(Buffer.concat(chunks));
|
||||
});
|
||||
req.on("error", rejectDone);
|
||||
req.end();
|
||||
|
||||
const result = await done;
|
||||
expect(result.length).toBe(payloadSize);
|
||||
|
||||
client.close();
|
||||
} finally {
|
||||
srv.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user