From 06fa1ed5984008029dbf26369da947a6733843cd Mon Sep 17 00:00:00 2001 From: pfg Date: Wed, 14 May 2025 18:56:55 -0700 Subject: [PATCH] Add support for maxSendHeaderBlockLength in http2 client (#19642) --- src/bun.js/api/bun/h2_frame_parser.zig | 46 ++++++++++++------ src/js/node/http2.ts | 13 +++++ ...-http2-options-max-headers-block-length.js | 47 +++++++++++++++++++ 3 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 test/js/node/test/parallel/test-http2-options-max-headers-block-length.js diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 37ee14b190..ea48c7d695 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -32,12 +32,12 @@ pub fn getHTTP2CommonString(globalObject: *JSC.JSGlobalObject, hpack_index: u32) if (value.isEmptyOrUndefinedOrNull()) return null; return value; } -const MAX_WINDOW_SIZE = 2147483647; -const MAX_HEADER_TABLE_SIZE = 4294967295; -const MAX_STREAM_ID = 2147483647; -const WINDOW_INCREMENT_SIZE = 65536; -const MAX_HPACK_HEADER_SIZE = 65536; -const MAX_FRAME_SIZE = 16777215; +const MAX_WINDOW_SIZE = std.math.maxInt(i32); +const MAX_HEADER_TABLE_SIZE = std.math.maxInt(u32); +const MAX_STREAM_ID = std.math.maxInt(i32); +const WINDOW_INCREMENT_SIZE = std.math.maxInt(u16); +const MAX_HPACK_HEADER_SIZE = std.math.maxInt(u16); +const MAX_FRAME_SIZE = std.math.maxInt(u24); const PaddingStrategy = enum { none, @@ -235,14 +235,6 @@ const FullSettingsPayload = packed struct(u288) { return (writer.write(std.mem.asBytes(&swap)[0..FullSettingsPayload.byteSize]) catch 0) != 0; } }; -const ValidPseudoHeaders = bun.ComptimeStringMap(void, .{ - .{":status"}, - .{":method"}, - .{":authority"}, - .{":scheme"}, - .{":path"}, - .{":protocol"}, -}); const ValidResponsePseudoHeaders = bun.ComptimeStringMap(void, .{ .{":status"}, @@ -534,6 +526,7 @@ const Handlers = struct { onAborted: JSC.JSValue = .zero, onAltSvc: JSC.JSValue = .zero, onOrigin: JSC.JSValue = .zero, + onFrameError: JSC.JSValue = .zero, // Added for frameError events binary_type: BinaryType = .Buffer, vm: *JSC.VirtualMachine, @@ -592,6 +585,7 @@ const Handlers = struct { .{ "onWrite", "write" }, .{ "onAltSvc", "altsvc" }, .{ "onOrigin", "origin" }, + .{ "onFrameError", "frameError" }, }; inline for (pairs) |pair| { @@ -648,6 +642,7 @@ const Handlers = struct { this.onEnd = .zero; this.onGoAway = .zero; this.onAborted = .zero; + this.onFrameError = .zero; this.strong_ctx.deinit(); } }; @@ -701,6 +696,7 @@ pub const H2FrameParser = struct { queuedDataSize: u64 = 0, // this is in bytes maxOutstandingPings: u64 = 10, outStandingPings: u64 = 0, + maxSendHeaderBlockLength: u32 = 0, lastStreamID: u32 = 0, isServer: bool = false, prefaceReceivedLen: u8 = 0, @@ -3940,6 +3936,23 @@ pub const H2FrameParser = struct { } log("request encoded_size {}", .{encoded_size}); + + // Check if headers block exceeds maxSendHeaderBlockLength + if (this.maxSendHeaderBlockLength != 0 and encoded_size > this.maxSendHeaderBlockLength) { + stream.state = .CLOSED; + stream.rstCode = @intFromEnum(ErrorCode.REFUSED_STREAM); + + this.dispatchWith2Extra( + .onFrameError, + stream.getIdentifier(), + JSC.JSValue.jsNumber(@intFromEnum(FrameType.HTTP_FRAME_HEADERS)), + JSC.JSValue.jsNumber(@intFromEnum(ErrorCode.FRAME_SIZE_ERROR)), + ); + + this.dispatchWithExtra(.onStreamError, stream.getIdentifier(), JSC.JSValue.jsNumber(stream.rstCode)); + return JSC.JSValue.jsNumber(stream_id); + } + const padding = stream.getPadding(encoded_size, buffer.len - 1); const payload_size = encoded_size + (if (padding != 0) padding + 1 else 0); if (padding != 0) { @@ -4204,6 +4217,11 @@ pub const H2FrameParser = struct { this.maxOutstandingSettings = @max(1, @as(u32, @truncate(max_outstanding_settings.to(u64)))); } } + if (try settings_js.get(globalObject, "maxSendHeaderBlockLength")) |max_send_header_block_length| { + if (max_send_header_block_length.isNumber()) { + this.maxSendHeaderBlockLength = @bitCast(max_send_header_block_length.toInt32()); + } + } } } var is_server = false; diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index ddfd7996d7..9e94325176 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -2479,6 +2479,11 @@ class ServerHttp2Session extends Http2Session { const stream = new ServerHttp2Stream(stream_id, self, null); self.#parser?.setStreamContext(stream_id, stream); }, + frameError(self: ServerHttp2Session, stream: ServerHttp2Stream, frameType: number, errorCode: number) { + if (!self || typeof stream !== "object") return; + // Emit the frameError event with the frame type and error code + process.nextTick(emitFrameErrorEventNT, stream, frameType, errorCode); + }, aborted(self: ServerHttp2Session, stream: ServerHttp2Stream, error: any, old_state: number) { if (!self || typeof stream !== "object") return; stream.rstCode = constants.NGHTTP2_CANCEL; @@ -2950,6 +2955,11 @@ class ClientHttp2Session extends Http2Session { self.#parser?.setStreamContext(stream_id, stream); } }, + frameError(self: ClientHttp2Session, stream: ClientHttp2Stream, frameType: number, errorCode: number) { + if (!self || typeof stream !== "object") return; + // Emit the frameError event with the frame type and error code + process.nextTick(emitFrameErrorEventNT, stream, frameType, errorCode); + }, aborted(self: ClientHttp2Session, stream: ClientHttp2Stream, error: any, old_state: number) { if (!self || typeof stream !== "object") return; stream.rstCode = constants.NGHTTP2_CANCEL; @@ -3699,6 +3709,9 @@ Http2Server.prototype[EventEmitter.captureRejectionSymbol] = function (err, even function onErrorSecureServerSession(err, socket) { if (!this.emit("clientError", err, socket)) socket.destroy(err); } +function emitFrameErrorEventNT(stream, frameType, errorCode) { + stream.emit("frameError", frameType, errorCode); +} class Http2SecureServer extends tls.Server { timeout = 0; constructor(options, onRequestHandler) { diff --git a/test/js/node/test/parallel/test-http2-options-max-headers-block-length.js b/test/js/node/test/parallel/test-http2-options-max-headers-block-length.js new file mode 100644 index 0000000000..0a2645c4cc --- /dev/null +++ b/test/js/node/test/parallel/test-http2-options-max-headers-block-length.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustNotCall()); +server.listen(0, common.mustCall(() => { + + // Setting the maxSendHeaderBlockLength, then attempting to send a + // headers block that is too big should cause a 'frameError' to + // be emitted, and will cause the stream to be shutdown. + const options = { + maxSendHeaderBlockLength: 10 + }; + + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + client.on('error', () => {}); + + const req = client.request(); + req.on('response', common.mustNotCall()); + + req.resume(); + req.on('close', common.mustCall(() => { + client.close(); + server.close(); + })); + + req.on('frameError', common.mustCall((type, code) => { + assert.strictEqual(code, h2.constants.NGHTTP2_FRAME_SIZE_ERROR); + })); + + // NGHTTP2 will automatically send the NGHTTP2_REFUSED_STREAM with + // the GOAWAY frame. + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + name: 'Error', + message: 'Stream closed with error code NGHTTP2_REFUSED_STREAM' + })); +}));