Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
9383d83
http: honor --insecure-http-parser, freeze writeHead framing, drop ov…
cirospaciari Jul 17, 2026
69fc04f
Merge remote-tracking branch 'origin/main' into ciro/node-http-v26-co…
cirospaciari Jul 17, 2026
c5a99f8
http: fix framing/leniency edge cases found in review
cirospaciari Jul 17, 2026
df4965e
http: run a connectionListener so emit('connection', socket) is served
cirospaciari Jul 17, 2026
7cf0e01
http2: serve the HTTP/1 half of protocol autoselection
cirospaciari Jul 17, 2026
7540792
http: bound request headers the way llhttp does
cirospaciari Jul 17, 2026
e800d5d
http: render the chunked Transfer-Encoding after Connection, like Node
cirospaciari Jul 17, 2026
1d5e39f
test: the 200 after a 304 is chunked, like Node
cirospaciari Jul 17, 2026
013e52f
http: honor the chunked Transfer-Encoding bit in the JS fallback
cirospaciari Jul 17, 2026
11588aa
Merge branch 'main' into ciro/node-http-v26-compat
cirospaciari Jul 17, 2026
a733f96
http: charge a header value's trailing whitespace, like llhttp
cirospaciari Jul 17, 2026
65377b9
Merge remote-tracking branch 'origin/ciro/node-http-v26-compat' into …
cirospaciari Jul 17, 2026
d0c0354
http: split parser leniency into llhttp's two bits
cirospaciari Jul 17, 2026
ac379e9
http: honor the auto-header bits exactly in the JS fallback
cirospaciari Jul 17, 2026
4371163
http: bound pipelined dispatch like Node's flood prevention
cirospaciari Jul 17, 2026
a91f521
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
b8b6be5
http: hand off Upgrade and CONNECT in the JS fallback
cirospaciari Jul 17, 2026
d9a4921
Merge remote-tracking branch 'origin/ciro/node-http-v26-compat' into …
cirospaciari Jul 17, 2026
09c0380
test: cover the fallback's Upgrade/CONNECT handoff
cirospaciari Jul 17, 2026
6e654b4
Merge branch 'main' into ciro/node-http-v26-compat
cirospaciari Jul 17, 2026
ce59cfe
address review: CONNECT double-delivery, spill-replay backpressure, f…
robobun Jul 17, 2026
a76d5a8
Merge branch 'main' into ciro/node-http-v26-compat; address ce59cfe r…
robobun Jul 17, 2026
6641668
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
363c67c
http2: fail the session when an outbound header block cannot be encoded
cirospaciari Jul 17, 2026
73065c5
http2: perf_hooks entries, END_STREAM on the final DATA frame, and cl…
cirospaciari Jul 17, 2026
b38891c
http2: pack END_STREAM onto end(chunk) and match node's frame accounting
cirospaciari Jul 17, 2026
958112f
http2: fix silent client stall when the socket flows before connect c…
cirospaciari Jul 17, 2026
f4a9f71
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
05ed257
h2: release auto-flush registration after deferred COMPRESSION_ERROR;…
robobun Jul 17, 2026
754d353
ci: retrigger
robobun Jul 18, 2026
9dd71e1
h2: keep pending_header_compression_error latched across the onError …
robobun Jul 18, 2026
3a54f48
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 18, 2026
4ce300b
fallback: add writeInformational so writeEarlyHints/writeProcessing r…
robobun Jul 18, 2026
be93d72
h2/fallback review: send_trailers encode failure -> session COMPRESSI…
robobun Jul 18, 2026
cb56096
test: wire must-not-run request handlers to reject/flag instead of th…
robobun Jul 18, 2026
6d0f17b
revert send_trailers to per-stream FRAME_SIZE_ERROR; fix onHttp1Socke…
robobun Jul 18, 2026
a227e00
Merge branch 'main' into ciro/node-http-v26-compat
cirospaciari Jul 20, 2026
8715a0e
Merge branch 'main' into ciro/node-http-v26-compat
robobun Jul 22, 2026
d361c02
docs: move rewrite_read doc comment back above its fn (sync_engine_fr…
robobun Jul 22, 2026
f641f77
fallback: map HPE_CHUNK_EXTENSIONS_OVERFLOW to 413 like Node's socket…
robobun Jul 22, 2026
9ac1cf6
Merge branch 'main' into ciro/node-http-v26-compat
cirospaciari Jul 22, 2026
77cbe93
http2: defer streamEnd(7) destroy when end(chunk)'s own _write drove …
robobun Jul 22, 2026
3c6783e
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 22, 2026
4485e5c
http2: stamp timeToFirstHeader in server streamHeaders too; fallback …
robobun Jul 22, 2026
1af5840
Merge branch 'main' into ciro/node-http-v26-compat
cirospaciari Jul 23, 2026
fe85cfb
Merge remote-tracking branch 'origin/main' into ciro/node-http-v26-co…
robobun Aug 3, 2026
4227bbf
NodeHTTP.cpp: hoist lenientHttpFlags.toInt32 and exception-check befo…
robobun Aug 3, 2026
b87e396
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
b85baf4
test(spawn-maxbuf): widen killWindow on ASAN builds too
robobun Aug 4, 2026
29b0954
Merge remote-tracking branch 'origin/main' into ciro/node-http-v26-co…
robobun Aug 4, 2026
9a12c62
Merge remote-tracking branch 'origin/main' into ciro/node-http-v26-co…
robobun Aug 4, 2026
64bcf70
node:http flood prevention: rename nodeHttpReadsPausedSignal -> nodeH…
robobun Aug 4, 2026
59c93f1
http2.ts: drop unused throwNotImplemented import and unused bunTLSCon…
robobun Aug 4, 2026
86d0bda
node:http flood prevention: hold the parked-request spill in a WTF::V…
robobun Aug 4, 2026
70833a7
http2: mark the push stream handed out on the unencodable-block path …
robobun Aug 4, 2026
e758733
test: restore node-http-proxy.js to its original localhost form
robobun Aug 4, 2026
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
7 changes: 5 additions & 2 deletions packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -781,10 +781,13 @@ struct TemplatedApp {
return std::move(*this);
}

TemplatedApp &&setFlags(bool requireHostHeader, bool useStrictMethodValidation, bool useInsecureHTTPParser, bool httpAllowHalfOpen) {
/* lenientHttpFlags: bit 0 = lenient header values (llhttp LENIENT_HEADERS),
* bit 1 = lenient transfer-encoding (llhttp LENIENT_TRANSFER_ENCODING). */
TemplatedApp &&setFlags(bool requireHostHeader, bool useStrictMethodValidation, uint8_t lenientHttpFlags, bool httpAllowHalfOpen) {
httpContext->getSocketContextData()->flags.requireHostHeader = requireHostHeader;
httpContext->getSocketContextData()->flags.useStrictMethodValidation = useStrictMethodValidation;
httpContext->getSocketContextData()->flags.useInsecureHTTPParser = useInsecureHTTPParser;
httpContext->getSocketContextData()->flags.useInsecureHTTPParser = (lenientHttpFlags & 1) != 0;
httpContext->getSocketContextData()->flags.useLenientTransferEncoding = (lenientHttpFlags & 2) != 0;
httpContext->getSocketContextData()->flags.httpAllowHalfOpen = httpAllowHalfOpen;
return std::move(*this);
}
Expand Down
32 changes: 27 additions & 5 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
#include <mutex>


extern "C" void Bun__NodeHTTP__onReadsResumable(int ssl, struct us_socket_t *s);

namespace uWS {

namespace detail {
Expand Down Expand Up @@ -129,6 +131,13 @@ struct HttpContext {
static unsigned char socketKind() { return SSL ? US_SOCKET_KIND_UWS_HTTP_TLS : US_SOCKET_KIND_UWS_HTTP; }

public:
/* node:http flood prevention: re-feed parked request bytes through the same
* parse path fresh socket data takes. The caller guarantees the buffer has
* LIBUS_RECV_BUFFER_PADDING of writable slack past `length`. */
static us_socket_t *feedNodeHttpData(us_socket_t *s, char *data, int length) {
return onData<true>(s, data, length);
}

us_socket_group_t *getSocketGroup() {
return &group;
}
Expand Down Expand Up @@ -342,7 +351,7 @@ struct HttpContext {
nodeHttpRequestTrailers = &nodeHttpResponseData->nodeHttpRequestTrailers;
}

auto result = httpResponseData->template consumePostPadded<IsNodeHttp>(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, httpContextData->flags.useInsecureHTTPParser, nodeHttpRequestTrailers, &httpResponseData->chunkedExtensionsByteCount, data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
auto result = httpResponseData->template consumePostPadded<IsNodeHttp>(httpContextData->maxHeaderSize, httpResponseData->isConnectRequest, httpContextData->flags.requireHostHeader,httpContextData->flags.useStrictMethodValidation, httpContextData->flags.useInsecureHTTPParser, httpContextData->flags.useLenientTransferEncoding, nodeHttpRequestTrailers, &httpResponseData->chunkedExtensionsByteCount, 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 */
Expand Down Expand Up @@ -400,6 +409,9 @@ struct HttpContext {
httpResponseData->nodeHttpQueuedPipelinedCount++;
if (((AsyncSocket<SSL> *) s)->getBufferedAmount() > 0) {
httpResponseData->state |= HttpResponseData<SSL>::HTTP_NODE_READS_PAUSED;
/* Also stop the request loop over the buffer being parsed
* right now — pausing the socket alone cannot bound it. */
httpResponseData->nodeHttpParkAtNextBoundary = true;
((HttpResponse<SSL> *) s)->pause();
}
}
Expand All @@ -426,6 +438,15 @@ struct HttpContext {
* on this keep-alive connection (the flag itself was cleared above). */
if constexpr (IsNodeHttp) {
((HttpResponseData<SSL, true> *) httpResponseData)->nodeHttpResponseTrailers.clear();

/* Node's flood prevention: sync write()+end() handlers bypass the pipelined
* branch yet still back up the socket. On outgoing backpressure, pause reads
* and park already-received requests. No already-paused guard (replay clears the park flag only). */
if (((AsyncSocket<SSL> *) s)->getBufferedAmount() > 0) {
httpResponseData->state |= HttpResponseData<SSL>::HTTP_NODE_READS_PAUSED;
httpResponseData->nodeHttpParkAtNextBoundary = true;
((HttpResponse<SSL> *) s)->pause();
}
}
}

Expand Down Expand Up @@ -735,10 +756,11 @@ struct HttpContext {
* backpressure when the queue drained; now that it has flushed, read
* new requests again. */
if constexpr (IsNodeHttp) {
if ((httpResponseData->state & HttpResponseData<SSL>::HTTP_NODE_READS_PAUSED) && httpResponseData->nodeHttpQueuedPipelinedCount == 0
&& asyncSocket->getBufferedAmount() == 0) {
httpResponseData->state &= ~HttpResponseData<SSL>::HTTP_NODE_READS_PAUSED;
reinterpret_cast<HttpResponse<SSL> *>(s)->resume();
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_NODE_READS_PAUSED) {
/* Parked pipelined requests must replay before fresh reads or the stream
* reorders; the hook holds under backpressure and resumes raw reads only once
* the queue and spill drain (JSNodeHTTPServerSocket.cpp). */
Bun__NodeHTTP__onReadsResumable(SSL, s);
Comment on lines +759 to +763

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robobun this branch is for onReadsPaused. is it correct to be calling onReadsResumable?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is inside onWritable, which fires when outgoing backpressure has drained. The check is "reads are currently paused" and the call is "reads may now be resumable, decide and act".

Bun__NodeHTTP__onReadsResumable (onNodeHttpReadsResumable<SSL> in JSNodeHTTPServerSocket.cpp) is the single decision point for whether to actually resume. It applies the same conditions the removed inline code did (getBufferedAmount() == 0, queued-pipeline state) plus the case the inline code could not handle: parked pipelined request bytes in nodeHttpPausedSpill. Those were received while reads were paused mid-parse and must be replayed through the parser before raw reads resume, or the request stream reorders. The hook:

  1. returns early if getBufferedAmount() > 0 (still backpressured);
  2. returns early if the spill is empty but nodeHttpQueuedPipelinedCount > 0 (queue-drain re-enters it later);
  3. if the spill is empty with no queue, clears HTTP_NODE_READS_PAUSED and calls resume() (same as the old inline code);
  4. otherwise posts a deferred task that re-feeds the spill through feedNodeHttpData and only resumes raw reads once the spill has drained without re-pausing.

Three call sites reach it (onWritable here, startPipelinedResponseImpl, and Rust's resume_socket) so the resume conditions live in one place instead of being duplicated inline at each.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — HTTP_NODE_READS_PAUSED is a state bit ("reads are currently paused"), not the event being fired. This is the onWritable path right after flush(), i.e. the outgoing backpressure that caused the pause just drained, so we notify the reads-resumable hook, which replays the parked spill and only then clears the bit and resume()s (see onNodeHttpReadsResumable in JSNodeHTTPServerSocket.cpp:444-499 — same if (state & HTTP_NODE_READS_PAUSED) onNodeHttpReadsResumable() pattern at :554). There is no onReadsPaused hook; pausing happens inline at HttpContext.h:407/451 via pause() + Bun__NodeHTTP__setReadsPausedSignal.

}
}

Expand Down
9 changes: 4 additions & 5 deletions packages/bun-uws/src/HttpContextData.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@ struct HttpFlags {
bool requireHostHeader: 1 = true;
bool isAuthorized: 1 = false;
bool useStrictMethodValidation: 1 = false;
/* node:http insecureHTTPParser server option. NOTE: unlike Node's server
* (which fans kLenientAll out to all 10 llhttp lenient setters), the uWS
* parser only implements the LENIENT_HEADERS bit (control bytes accepted
* in field values); TE+CL conflict, chunked-size/CRLF strictness, version
* and header-token checks are still enforced. */
/* node:http parser leniency. Two llhttp lenient bits: useInsecureHTTPParser = LENIENT_HEADERS
* ("relaxed"+"insecure"); useLenientTransferEncoding = LENIENT_TRANSFER_ENCODING ("insecure"
* only). TE+CL conflict, chunked-size/CRLF, version, header-token checks stay enforced. */
bool useInsecureHTTPParser: 1 = false;
bool useLenientTransferEncoding: 1 = false;
/* node:http server.httpAllowHalfOpen: when true, a peer FIN with in-flight
* or queued responses keeps the connection open until they drain (Node's
* socketOnEnd); when false (the default), the connection ends right away. */
Expand Down
69 changes: 58 additions & 11 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <string_view>
#include <span>
#include <map>
#include <wtf/Vector.h>
#include "MoveOnlyFunction.h"
#include "ChunkedEncoding.h"

Expand Down Expand Up @@ -604,6 +605,14 @@ struct HttpResponseData;

private:
std::string fallback;
public:
/* node:http flood prevention. HTTP_NODE_READS_PAUSED (state bit) = the socket's raw reads are
* paused and stays set through spill replay; this flag = "the parse loop running now must stop
* at the next request boundary and park the rest", cleared for replay so it can make progress. */
bool nodeHttpParkAtNextBoundary = false;
bool nodeHttpSpillReplayScheduled = false;
WTF::Vector<char> nodeHttpPausedSpill;
private:
/* This guy really has only 30 bits since we reserve two highest bits to chunked encoding parsing state */
uint64_t remainingStreamingBytes = 0;
/* node:http compat: a completed request on this connection forbade keep-alive
Expand All @@ -612,6 +621,10 @@ struct HttpResponseData;
bool nodeHttpSawConnectionClose = false;

const size_t MAX_FALLBACK_SIZE = BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;
/* maxHeaderSize bounds what llhttp counts (URL + field names/values), not framing
* (method, " HTTP/1.1\r\n", ": ", "\r\n"). Raw bounds get that framing as slack so
* we don't reject requests Node accepts. Finite: ≤UWS_HTTP_MAX_HEADERS_COUNT*4 + 64. */
static constexpr size_t MAX_HEADER_FRAMING_SLACK = UWS_HTTP_MAX_HEADERS_COUNT * 4 + 64;

/* Maximum chunk-extension bytes per chunk, matching Node/llhttp's
* kMaxChunkExtensionsSize (16 KiB). Enforced for every server
Expand Down Expand Up @@ -984,8 +997,13 @@ struct HttpResponseData;
if(requestLineResult.isConnect) {
isConnectRequest = true;
}
/* No request headers found */
const char * headerStart = (headers[0].key.length() > 0) ? headers[0].key.data() : end;
/* Mirror llhttp's TrackHeader: accumulate URL + name + value lengths only (llhttp
* never charges method/separators/CRLF) and fail at maxHeaderSize. The fallback
* buffer keeps its own raw bound (maxBufferedHeaderSize). github.com/nodejs/llhttp */
uint64_t headerNread = headers[0].value.length();
if (maxHeaderSize && headerNread >= maxHeaderSize) {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}

/* Check if we can see if headers follow or not */
if (postPaddedBuffer + 2 > end) {
Expand All @@ -1007,7 +1025,8 @@ struct HttpResponseData;
preliminaryKey = postPaddedBuffer;
postPaddedBuffer = consumeFieldName(postPaddedBuffer);
headers->key = std::string_view(preliminaryKey, (size_t) (postPaddedBuffer - preliminaryKey));
if(maxHeaderSize && (uintptr_t)(postPaddedBuffer - headerStart) > maxHeaderSize) {
headerNread += headers->key.length();
if(maxHeaderSize && headerNread >= maxHeaderSize) {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}
/* We should not accept whitespace between key and colon, so colon must foloow immediately */
Expand Down Expand Up @@ -1050,7 +1069,14 @@ struct HttpResponseData;
}
break;
}
if(maxHeaderSize && (uintptr_t)(postPaddedBuffer - headerStart) > maxHeaderSize) {
/* Bound the value before its terminator is found — an unterminated oversized
* value must overflow here (where llhttp would), not wait for more data.
* llhttp sees the value with leading OWS skipped, so that OWS is not charged. */
const char *countedValueStart = preliminaryValue;
while (countedValueStart < postPaddedBuffer && isHTTPHeaderValueWhitespace((unsigned char) *countedValueStart)) {
countedValueStart++;
}
if(maxHeaderSize && headerNread + (uintptr_t)(postPaddedBuffer - countedValueStart) >= maxHeaderSize) {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}
if (end - postPaddedBuffer < 2) {
Expand All @@ -1061,6 +1087,10 @@ struct HttpResponseData;
if (postPaddedBuffer[1] == '\n') {
/* Store this header, it is valid */
headers->value = std::string_view(preliminaryValue, (size_t) (postPaddedBuffer - preliminaryValue));
/* Charge like llhttp's on_header_value: leading OWS skipped, trailing OWS
* counted. Measure before the trims below or trailing-space-padded values
* are undercharged and we accept a header block Node answers with 431. */
const size_t chargedValueLength = (size_t) (postPaddedBuffer - countedValueStart);
postPaddedBuffer += 2;
/* Trim trailing whitespace (SP, HTAB) per RFC 9110 Section 5.5 */
while (headers->value.length() && isHTTPHeaderValueWhitespace(headers->value.back())) {
Expand All @@ -1072,7 +1102,8 @@ struct HttpResponseData;
headers->value.remove_prefix(1);
}

if(maxHeaderSize && (uintptr_t)(postPaddedBuffer - headerStart) > maxHeaderSize) {
headerNread += chargedValueLength;
if(maxHeaderSize && headerNread >= maxHeaderSize) {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}
headers++;
Expand Down Expand Up @@ -1108,7 +1139,7 @@ struct HttpResponseData;

/* This is the only caller of getHeaders and is thus the deepest part of the parser. */
template <bool ConsumeMinimally, bool IsNodeHttp>
HttpParserResult fenceAndConsumePostPadded(uint64_t maxHeaderSize, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, bool useInsecureHTTPParser, std::string *nodeHttpRequestTrailers, uint64_t *chunkedExtensionsByteCount, char *data, unsigned int length, void *user, void *reserved, HttpRequest *req, MoveOnlyFunction<void *(void *, HttpRequest *)> &requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &dataHandler) {
HttpParserResult fenceAndConsumePostPadded(uint64_t maxHeaderSize, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, bool useInsecureHTTPParser, bool useLenientTransferEncoding, std::string *nodeHttpRequestTrailers, uint64_t *chunkedExtensionsByteCount, char *data, unsigned int length, void *user, void *reserved, HttpRequest *req, MoveOnlyFunction<void *(void *, HttpRequest *)> &requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &dataHandler) {

/* How much data we CONSUMED (to throw away) */
unsigned int consumedTotal = 0;
Expand All @@ -1129,6 +1160,16 @@ struct HttpResponseData;
consumedTotal += length;
return HttpParserResult::success(consumedTotal, returnedUser);
}
/* node:http flood prevention: a dispatch earlier in this buffer paused reads.
* Stop at this request boundary, park the rest, report it as consumed so the
* caller does not spill it into the size-capped header fallback buffer. */
if constexpr (IsNodeHttp) {
if (nodeHttpParkAtNextBoundary) [[unlikely]] {
nodeHttpPausedSpill.append(std::span<const char>(data, length));
consumedTotal += length;
return HttpParserResult::success(consumedTotal, user);
}
}
/* RFC 9112 2.2: ignore empty lines (CRLF) received prior to the
* request-line, like Node/llhttp - e.g. a stray "\r\n" sent on an
* idle keep-alive connection must not be treated as a bad request.
Expand Down Expand Up @@ -1164,7 +1205,7 @@ struct HttpResponseData;
consumedTotal += consumed;

/* Even if we could parse it, check for length here as well */
const uint64_t maxBufferedHeaderSize = maxHeaderSize ? maxHeaderSize : MAX_FALLBACK_SIZE;
const uint64_t maxBufferedHeaderSize = maxHeaderSize ? (maxHeaderSize + MAX_HEADER_FRAMING_SLACK) : MAX_FALLBACK_SIZE;
if (consumed > maxBufferedHeaderSize) {
return HttpParserResult::error(HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE);
}
Expand Down Expand Up @@ -1226,6 +1267,12 @@ struct HttpResponseData;
bool deferredTransferEncodingError = IsNodeHttp && transferEncoding.has
&& !transferEncoding.invalid && !transferEncoding.chunked && !contentLengthStringLen;

/* llhttp LENIENT_TRANSFER_ENCODING (kLenientAll / "insecure", never "relaxed")
* accepts chunked with another value after it. It does not relax the TE+CL
* conflict, so only the coding-shape verdict is cleared; conflicts below still reject. */
if (useLenientTransferEncoding) {
transferEncoding.invalid = false;
}
/* Bun.serve: no transfer coding other than chunked is implemented, so a
* list that ends in chunked but also names another coding ("gzip, chunked",
* "x, chunked", two TE fields) would hand the still-encoded body to the
Expand Down Expand Up @@ -1390,10 +1437,10 @@ struct HttpResponseData;

public:
template <bool IsNodeHttp>
HttpParserResult consumePostPadded(uint64_t maxHeaderSize, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, bool useInsecureHTTPParser, std::string *nodeHttpRequestTrailers, uint64_t *chunkedExtensionsByteCount, 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, bool& isConnectRequest, bool requireHostHeader, bool useStrictMethodValidation, bool useInsecureHTTPParser, bool useLenientTransferEncoding, std::string *nodeHttpRequestTrailers, uint64_t *chunkedExtensionsByteCount, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
/* The fallback buffer may not exceed the configured per-request header
* limit (per-server maxHeaderSize can raise it above the default). */
const size_t maxFallbackSize = maxHeaderSize ? (size_t) maxHeaderSize : MAX_FALLBACK_SIZE;
const size_t maxFallbackSize = maxHeaderSize ? (size_t) (maxHeaderSize + MAX_HEADER_FRAMING_SLACK) : MAX_FALLBACK_SIZE;
/* 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;
Expand Down Expand Up @@ -1466,7 +1513,7 @@ struct HttpResponseData;
fallback.append(data, maxCopyDistance);

// break here on break
HttpParserResult consumed = fenceAndConsumePostPadded<true, IsNodeHttp>(maxHeaderSize, isConnectRequest, requireHostHeader, useStrictMethodValidation, useInsecureHTTPParser, nodeHttpRequestTrailers, chunkedExtensionsByteCount, fallback.data(), (unsigned int) fallback.length(), user, reserved, &req, requestHandler, dataHandler);
HttpParserResult consumed = fenceAndConsumePostPadded<true, IsNodeHttp>(maxHeaderSize, isConnectRequest, requireHostHeader, useStrictMethodValidation, useInsecureHTTPParser, useLenientTransferEncoding, nodeHttpRequestTrailers, chunkedExtensionsByteCount, fallback.data(), (unsigned int) fallback.length(), user, reserved, &req, requestHandler, dataHandler);
/* Return data will be different than user if we are upgraded to WebSocket or have an error */
if (consumed.returnedData != user) {
return consumed;
Expand Down Expand Up @@ -1548,7 +1595,7 @@ struct HttpResponseData;
}
}

HttpParserResult consumed = fenceAndConsumePostPadded<false, IsNodeHttp>(maxHeaderSize, isConnectRequest, requireHostHeader, useStrictMethodValidation, useInsecureHTTPParser, nodeHttpRequestTrailers, chunkedExtensionsByteCount, data, length, user, reserved, &req, requestHandler, dataHandler);
HttpParserResult consumed = fenceAndConsumePostPadded<false, IsNodeHttp>(maxHeaderSize, isConnectRequest, requireHostHeader, useStrictMethodValidation, useInsecureHTTPParser, useLenientTransferEncoding, nodeHttpRequestTrailers, chunkedExtensionsByteCount, data, length, user, reserved, &req, requestHandler, dataHandler);
/* Return data will be different than user if we are upgraded to WebSocket or have an error */
if (consumed.returnedData != user) {
return consumed;
Expand Down
Loading