Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
785976d25c [autofix.ci] apply automated fixes 2026-01-15 01:26:07 +00:00
Claude Bot
66b96948a4 feat(http): add --max-http-header-count CLI flag
Adds a configurable maximum HTTP header count limit, similar to
--max-http-header-size. This allows servers to accept requests with
more than the default 100 headers.

Features:
- CLI flag: --max-http-header-count <INT> (default: 100)
- Node.js API: http.maxHeadersCount getter/setter
- Dynamic header allocation when limit > 100 (fast path for default)
- Returns HTTP 431 when header count exceeds limit

Fixes #6982

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 01:15:20 +00:00
12 changed files with 263 additions and 8 deletions

View File

@@ -666,6 +666,11 @@ public:
return std::move(*this);
}
TemplatedApp &&setMaxHTTPHeadersCount(uint32_t maxHeadersCount) {
httpContext->getSocketContextData()->maxHeadersCount = maxHeadersCount;
return std::move(*this);
}
};
typedef TemplatedApp<false> App;

View File

@@ -243,7 +243,7 @@ private:
/* The return value is entirely up to us to interpret. The HttpParser cares only for whether the returned value is DIFFERENT from passed user */
auto result = httpResponseData->consumePostPadded(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
auto result = httpResponseData->consumePostPadded(httpContextData->maxHeaderSize, httpContextData->maxHeadersCount, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
/* For every request we reset the timeout and hang until user makes action */

View File

@@ -70,6 +70,7 @@ private:
OnClientErrorCallback onClientError = nullptr;
uint64_t maxHeaderSize = 0; // 0 means no limit
uint32_t maxHeadersCount = 0; // 0 means use default (UWS_HTTP_MAX_HEADERS_COUNT)
// TODO: SNI
void clearRoutes() {

View File

@@ -45,6 +45,7 @@
#endif
extern "C" size_t BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;
extern "C" uint32_t BUN_DEFAULT_MAX_HTTP_HEADERS_COUNT;
extern "C" int16_t Bun__HTTPMethod__from(const char *str, size_t len);
namespace uWS
@@ -148,11 +149,22 @@ namespace uWS
friend struct HttpParser;
private:
public:
struct Header
{
std::string_view key, value;
} headers[UWS_HTTP_MAX_HEADERS_COUNT];
};
private:
/* Stack-allocated headers for the common case (fast path) */
Header stackHeaders[UWS_HTTP_MAX_HEADERS_COUNT];
/* Heap-allocated headers when maxHeadersCount > UWS_HTTP_MAX_HEADERS_COUNT */
std::vector<Header> dynamicHeaders;
/* Points to either stackHeaders or dynamicHeaders.data() */
Header *headers = stackHeaders;
/* Current max headers count limit */
uint32_t maxHeadersCount = UWS_HTTP_MAX_HEADERS_COUNT;
bool ancientHttp;
bool didYield;
unsigned int querySeparator;
@@ -161,6 +173,23 @@ namespace uWS
std::map<std::string, unsigned short, std::less<>> *currentParameterOffsets = nullptr;
public:
/* Configure max headers count - must be called before parsing */
void setMaxHeadersCount(uint32_t count) {
if (count == 0) {
count = BUN_DEFAULT_MAX_HTTP_HEADERS_COUNT;
}
maxHeadersCount = count;
if (count > UWS_HTTP_MAX_HEADERS_COUNT) {
dynamicHeaders.resize(count);
headers = dynamicHeaders.data();
} else {
headers = stackHeaders;
}
}
uint32_t getMaxHeadersCount() const {
return maxHeadersCount;
}
/* Any data pipelined after the HTTP headers (before response).
* Used for Node.js compatibility: 'connect' and 'upgrade' events
* pass this as the 'head' Buffer parameter.
@@ -651,7 +680,7 @@ namespace uWS
}
/* End is only used for the proxy parser. The HTTP parser recognizes "\ra" as invalid "\r\n" scan and breaks. */
static HttpParserResult getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, bool &isAncientHTTP, bool &isConnectRequest, bool useStrictMethodValidation, uint64_t maxHeaderSize) {
static HttpParserResult getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, bool &isAncientHTTP, bool &isConnectRequest, bool useStrictMethodValidation, uint64_t maxHeaderSize, uint32_t maxHeadersCount) {
char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer;
#ifdef UWS_WITH_PROXY
/* ProxyParser is passed as reserved parameter */
@@ -725,7 +754,7 @@ namespace uWS
headers++;
for (unsigned int i = 1; i < UWS_HTTP_MAX_HEADERS_COUNT - 1; i++) {
for (unsigned int i = 1; i < maxHeadersCount - 1; i++) {
/* Lower case and consume the field name */
preliminaryKey = postPaddedBuffer;
postPaddedBuffer = consumeFieldName(postPaddedBuffer);
@@ -828,7 +857,7 @@ namespace uWS
data[length + 1] = 'a'; /* Anything that is not \n, to trigger "invalid request" */
req->ancientHttp = false;
for (;length;) {
auto result = getHeaders(data, data + length, req->headers, reserved, req->ancientHttp, isConnectRequest, useStrictMethodValidation, maxHeaderSize);
auto result = getHeaders(data, data + length, req->headers, reserved, req->ancientHttp, isConnectRequest, useStrictMethodValidation, maxHeaderSize, req->getMaxHeadersCount());
if(result.isError()) {
return result;
}
@@ -960,10 +989,12 @@ namespace uWS
}
public:
HttpParserResult consumePostPadded(uint64_t maxHeaderSize, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
HttpParserResult consumePostPadded(uint64_t maxHeaderSize, uint32_t maxHeadersCount, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
/* This resets BloomFilter by construction, but later we also reset it again.
* Optimize this to skip resetting twice (req could be made global) */
HttpRequest req;
/* Configure max headers count - enables dynamic allocation when > UWS_HTTP_MAX_HEADERS_COUNT */
req.setMaxHeadersCount(maxHeadersCount);
if (remainingStreamingBytes) {
if (isConnectRequest) {
dataHandler(user, std::string_view(data, length), false);

View File

@@ -40,5 +40,31 @@ pub fn setMaxHTTPHeaderSize(globalThis: *jsc.JSGlobalObject, callframe: *jsc.Cal
return jsc.JSValue.jsNumber(bun.http.max_http_header_size);
}
pub fn getMaxHTTPHeadersCount(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
_ = globalThis;
_ = callframe;
return jsc.JSValue.jsNumber(bun.http.max_http_headers_count);
}
pub fn setMaxHTTPHeadersCount(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const arguments = callframe.arguments_old(1).slice();
if (arguments.len < 1) {
return globalThis.throwNotEnoughArguments("setMaxHTTPHeadersCount", 1, arguments.len);
}
const value = arguments[0];
const num = try value.coerceToInt64(globalThis);
if (num < 0) {
return globalThis.throwInvalidArgumentTypeValue("maxHeadersCount", "non-negative integer", value);
}
if (num == 0) {
bun.http.max_http_headers_count = std.math.maxInt(u32);
} else {
bun.http.max_http_headers_count = @intCast(num);
}
return jsc.JSValue.jsNumber(bun.http.max_http_headers_count);
}
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;

View File

@@ -103,6 +103,7 @@ pub const runtime_params_ = [_]ParamType{
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
clap.parseParam("--fetch-preconnect <STR>... Preconnect to a URL while code is loading") catch unreachable,
clap.parseParam("--max-http-header-size <INT> Set the maximum size of HTTP headers in bytes. Default is 16KiB") catch unreachable,
clap.parseParam("--max-http-header-count <INT> Set the maximum number of HTTP headers. Default is 100") catch unreachable,
clap.parseParam("--dns-result-order <STR> Set the default order of DNS lookup results. Valid orders: verbatim (default), ipv4first, ipv6first") catch unreachable,
clap.parseParam("--expose-gc Expose gc() on the global object. Has no effect on Bun.gc().") catch unreachable,
clap.parseParam("--no-deprecation Suppress all reporting of the custom deprecation.") catch unreachable,
@@ -735,6 +736,18 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
}
if (args.option("--max-http-header-count")) |count_str| {
const count = std.fmt.parseInt(u32, count_str, 10) catch {
Output.errGeneric("Invalid value for --max-http-header-count: \"{s}\". Must be a positive integer\n", .{count_str});
Global.exit(1);
};
if (count == 0) {
bun.http.max_http_headers_count = std.math.maxInt(u32);
} else {
bun.http.max_http_headers_count = count;
}
}
if (args.option("--user-agent")) |user_agent| {
bun.http.overridden_default_user_agent = user_agent;
}

View File

@@ -531,6 +531,15 @@ extern "C"
uwsApp->setMaxHTTPHeaderSize(max_header_size);
}
}
void uws_app_set_max_http_headers_count(int ssl, uws_app_t *app, uint32_t max_headers_count) {
if (ssl) {
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;
uwsApp->setMaxHTTPHeadersCount(max_headers_count);
} else {
uWS::App *uwsApp = (uWS::App *)app;
uwsApp->setMaxHTTPHeadersCount(max_headers_count);
}
}
void uws_app_set_flags(int ssl, uws_app_t *app, bool require_host_header, bool use_strict_method_validation) {
if (ssl) {
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;

View File

@@ -63,6 +63,10 @@ pub fn NewApp(comptime ssl: bool) type {
return c.uws_app_set_max_http_header_size(ssl_flag, @as(*uws_app_t, @ptrCast(this)), max_header_size);
}
pub fn setMaxHTTPHeadersCount(this: *ThisApp, max_headers_count: u32) void {
return c.uws_app_set_max_http_headers_count(ssl_flag, @as(*uws_app_t, @ptrCast(this)), max_headers_count);
}
pub fn clearRoutes(app: *ThisApp) void {
return c.uws_app_clear_routes(ssl_flag, @as(*uws_app_t, @ptrCast(app)));
}
@@ -404,6 +408,7 @@ pub const c = struct {
pub extern fn uws_app_destroy(ssl: i32, app: *uws_app_t) void;
pub extern fn uws_app_set_flags(ssl: i32, app: *uws_app_t, require_host_header: bool, use_strict_method_validation: bool) void;
pub extern fn uws_app_set_max_http_header_size(ssl: i32, app: *uws_app_t, max_header_size: u64) void;
pub extern fn uws_app_set_max_http_headers_count(ssl: i32, app: *uws_app_t, max_headers_count: u32) void;
pub extern fn uws_app_get(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void;
pub extern fn uws_app_post(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void;
pub extern fn uws_app_options(ssl: i32, app: *uws_app_t, pattern: [*]const u8, pattern_len: usize, handler: uws_method_handler, user_data: ?*anyopaque) void;

View File

@@ -15,6 +15,11 @@ comptime {
@export(&max_http_header_size, .{ .name = "BUN_DEFAULT_MAX_HTTP_HEADER_SIZE" });
}
pub var max_http_headers_count: u32 = 100;
comptime {
@export(&max_http_headers_count, .{ .name = "BUN_DEFAULT_MAX_HTTP_HEADERS_COUNT" });
}
pub var overridden_default_user_agent: []const u8 = "";
const print_every = 0;

View File

@@ -352,6 +352,8 @@ function emitErrorNt(msg, err, callback) {
}
const setMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "setMaxHTTPHeaderSize", 1);
const getMaxHTTPHeaderSize = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeaderSize", 0);
const setMaxHTTPHeadersCount = $newZigFunction("node_http_binding.zig", "setMaxHTTPHeadersCount", 1);
const getMaxHTTPHeadersCount = $newZigFunction("node_http_binding.zig", "getMaxHTTPHeadersCount", 0);
const kOutHeaders = Symbol("kOutHeaders");
function ipToInt(ip) {
@@ -502,6 +504,7 @@ export {
getHeader,
getIsNextIncomingMessageHTTPS,
getMaxHTTPHeaderSize,
getMaxHTTPHeadersCount,
getRawKeys,
hasServerResponseFinished,
headerStateSymbol,
@@ -551,6 +554,7 @@ export {
setHeader,
setIsNextIncomingMessageHTTPS,
setMaxHTTPHeaderSize,
setMaxHTTPHeadersCount,
setRequestTimeout,
setServerCustomOptions,
setServerIdleTimeout,

View File

@@ -6,7 +6,14 @@ const { IncomingMessage } = require("node:_http_incoming");
const { OutgoingMessage } = require("node:_http_outgoing");
const { Server, ServerResponse } = require("node:_http_server");
const { METHODS, STATUS_CODES, setMaxHTTPHeaderSize, getMaxHTTPHeaderSize } = require("internal/http");
const {
METHODS,
STATUS_CODES,
setMaxHTTPHeaderSize,
getMaxHTTPHeaderSize,
setMaxHTTPHeadersCount,
getMaxHTTPHeadersCount,
} = require("internal/http");
const { WebSocket, CloseEvent, MessageEvent } = globalThis;
@@ -54,6 +61,12 @@ const http_exports = {
set maxHeaderSize(value) {
setMaxHTTPHeaderSize(value);
},
get maxHeadersCount() {
return getMaxHTTPHeadersCount();
},
set maxHeadersCount(value) {
setMaxHTTPHeadersCount(value);
},
validateHeaderName,
validateHeaderValue,
setMaxIdleHTTPParsers(max) {

View File

@@ -0,0 +1,143 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
describe("--max-http-header-count", () => {
test("http.maxHeadersCount getter returns default value", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", "console.log(require('http').maxHeadersCount)"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("100");
expect(exitCode).toBe(0);
});
test("http.maxHeadersCount setter works", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require('http');
console.log(http.maxHeadersCount);
http.maxHeadersCount = 500;
console.log(http.maxHeadersCount);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lines = stdout.trim().split("\n");
expect(lines[0]).toBe("100");
expect(lines[1]).toBe("500");
expect(exitCode).toBe(0);
});
test("--max-http-header-count CLI flag sets the value", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "--max-http-header-count=200", "-e", "console.log(require('http').maxHeadersCount)"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("200");
expect(exitCode).toBe(0);
});
test("server accepts requests with many headers when limit is increased", async () => {
using dir = tempDir("header-count-test", {
"server.ts": `
const server = Bun.serve({
port: 0,
fetch(req) {
const count = [...req.headers].length;
return new Response(String(count));
},
});
console.log(server.url.href);
`,
});
// Start server with higher header limit
await using proc = Bun.spawn({
cmd: [bunExe(), "--max-http-header-count=200", "server.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
// Read server URL from stdout
const reader = proc.stdout.getReader();
const { value } = await reader.read();
reader.releaseLock();
const url = new TextDecoder().decode(value).trim();
// Build request with 150 headers
const headers = new Headers();
for (let i = 0; i < 150; i++) {
headers.set(`X-Custom-Header-${i}`, `value-${i}`);
}
const res = await fetch(url, { headers });
expect(res.status).toBe(200);
const count = parseInt(await res.text());
// Account for default headers that fetch adds (Host, Accept, etc.)
expect(count).toBeGreaterThanOrEqual(150);
proc.kill();
});
test("server rejects requests with too many headers", async () => {
using dir = tempDir("header-count-reject-test", {
"server.ts": `
const server = Bun.serve({
port: 0,
fetch(req) {
const count = [...req.headers].length;
return new Response(String(count));
},
});
console.log(server.url.href);
`,
});
// Start server with low header limit (50)
await using proc = Bun.spawn({
cmd: [bunExe(), "--max-http-header-count=50", "server.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
// Read server URL from stdout
const reader = proc.stdout.getReader();
const { value } = await reader.read();
reader.releaseLock();
const url = new TextDecoder().decode(value).trim();
// Build request with 60 headers (exceeds limit)
const headers = new Headers();
for (let i = 0; i < 60; i++) {
headers.set(`X-Custom-Header-${i}`, `value-${i}`);
}
const res = await fetch(url, { headers });
// Should get 431 Request Header Fields Too Large
expect(res.status).toBe(431);
proc.kill();
});
});