Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 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 @@ -772,10 +772,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
42 changes: 37 additions & 5 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
#include <mutex>


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

namespace uWS {

namespace detail {
Expand Down Expand Up @@ -130,6 +132,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 @@ -337,7 +346,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 @@ -395,6 +404,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->nodeHttpReadsPausedSignal = true;
((HttpResponse<SSL> *) s)->pause();
}
}
Expand All @@ -421,6 +433,24 @@ 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 applies here too: a handler that
* write()s and end()s synchronously completes each exchange
* before the next dispatch, so the pipelined branch above
* never runs — yet unflushed response bytes pile up on the
* connection all the same. Once the socket carries outgoing
* backpressure, stop reading (and stop consuming the
* already-received requests: the signal parks them) until
* onWritable drains it. This request still dispatches, like
* Node, which pauses from within parserOnIncoming. No
* already-paused guard: the replay clears the signal but not
* the state bit, so gating on the bit would let the whole
* spill dispatch unbounded on the first replay. */
if (((AsyncSocket<SSL> *) s)->getBufferedAmount() > 0) {
httpResponseData->state |= HttpResponseData<SSL>::HTTP_NODE_READS_PAUSED;
httpResponseData->nodeHttpReadsPausedSignal = true;
((HttpResponse<SSL> *) s)->pause();
}
}
}

Expand Down Expand Up @@ -711,10 +741,12 @@ 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 be replayed before the socket
* reads fresh bytes, or the stream reorders; the hook holds
* under outgoing backpressure and resumes raw reads only once
* the queue and the 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
15 changes: 10 additions & 5 deletions packages/bun-uws/src/HttpContextData.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ 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 of llhttp's lenient bits are implemented:
* useInsecureHTTPParser is LENIENT_HEADERS (control bytes accepted in field
* values) — set for both httpValidation "relaxed" and "insecure" —
* and useLenientTransferEncoding is LENIENT_TRANSFER_ENCODING (a chunked
* coding with another value after it, e.g. a duplicate Transfer-Encoding:
* chunked header, is accepted) — set only for "insecure" /
* --insecure-http-parser, never for "relaxed", which must relax header
* values alone. The TE+CL conflict, chunked-size/CRLF strictness, version
* and header-token checks are still enforced under both. */
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
Loading
Loading