Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions src/runtime/api/bun/h2_frame_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6869,7 +6869,7 @@ impl H2FrameParser {
if !error_code_arg.is_number() {
return Err(global_object.throw(format_args!("Expected errorCode to be a number")));
}
let error_code = error_code_arg.to_int32();
let error_code = ErrorCode(error_code_arg.to_u32());

let mut last_stream_id = this.last_peer_stream_id.get();
if callframe.arguments_count() >= 2 {
Expand All @@ -6894,20 +6894,14 @@ impl H2FrameParser {
if let Some(array_buffer) = opaque_data_arg.as_array_buffer(global_object) {
// Own the bytes: write() re-enters JS on JS-backed sockets and can detach this.
let copied = array_buffer.byte_slice().to_vec();
this.send_go_away(
0,
ErrorCode(error_code as u32),
&copied,
last_stream_id,
false,
);
this.send_go_away(0, error_code, &copied, last_stream_id, false);
return Ok(JSValue::UNDEFINED);
}
}
}
}

this.send_go_away(0, ErrorCode(error_code as u32), b"", last_stream_id, false);
this.send_go_away(0, error_code, b"", last_stream_id, false);
Ok(JSValue::UNDEFINED)
}

Expand Down
40 changes: 40 additions & 0 deletions test/js/node/http2/node-http2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,46 @@ it("http2 session.goaway() sends custom data", async done => {
});
});

// GOAWAY's error code is an unsigned 32-bit field: codes above 2^31-1 must reach the peer as
// passed (node reads the argument with Uint32Value). Client and server sessions share the native
// goaway(), and opaqueData selects between its two send paths, so the cases vary both.
it.each([
["server", 0xffffffff, Buffer.from([0xde, 0xad, 0xbe, 0xef])],
["server", 0x80000000, undefined],
["client", 0xffffffff, undefined],
["client", 0x80000000, Buffer.from([0xde, 0xad, 0xbe, 0xef])],
])("http2 %s session.goaway() sends an error code of %d unchanged", async (sender, code, opaqueData) => {
const { promise: goawayReceived, resolve: onGoaway, reject } = Promise.withResolvers();
const onGoawayEvent = (receivedCode, lastStreamID, receivedData) =>
onGoaway({ code: receivedCode, lastStreamID, opaqueData: receivedData });
// The receiving side emits 'goaway' before it tears the session down with ERR_HTTP2_SESSION_ERROR,
// so the error and close events below only settle the promise when the frame never arrived.
const server = http2.createServer();
server.on("sessionError", reject);
server.on("session", session => {
session.on("close", () => reject(new Error("server session closed before 'goaway'")));
if (sender === "server") session.goaway(code, 0, opaqueData);
else session.on("goaway", onGoawayEvent);
});
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));

const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
client.on("error", reject);
client.on("close", () => reject(new Error("client session closed before 'goaway'")));
if (sender === "client") client.once("connect", () => client.goaway(code, 0, opaqueData));
else client.on("goaway", onGoawayEvent);
try {
expect(await goawayReceived).toEqual({
code,
lastStreamID: 0,
opaqueData: opaqueData ?? Buffer.alloc(0),
});
} finally {
client.destroy();
server.close();
}
});

it("http2 session.goaway() opaqueData survives re-entrant buffer detach over a JS Duplex", async () => {
// The cork-flush path re-enters JS (onWrite → Duplex _write) when the transport is a JS
// stream. If that JS detaches the opaqueData ArrayBuffer mid-write, the GOAWAY payload must
Expand Down