Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cfe3b6b
http: emit 'upgrade' event on 101 and honor chunked body on upgrade
robobun Apr 8, 2026
b34b02f
http: address review — post-upgrade framing, body reader lock, backpr…
robobun Apr 8, 2026
cb84d8d
http: derive request_body_has_framing from the finalized wire headers
robobun Apr 8, 2026
ed1878d
http(upgrade): close race windows around res/req destruction
robobun Apr 8, 2026
7f9c2ab
http(upgrade): don't conflate body-has-framing with body-is-chunked
robobun Apr 8, 2026
0cc82eb
http(upgrade): surface lost writes on close-before-drain; fix stale c…
robobun Apr 8, 2026
1c41795
http(upgrade): don't race req.close against backpressured socket writes
robobun Apr 8, 2026
7ffa9c8
http(upgrade): keep body generator alive for WebSocket/CDP pattern
robobun Apr 26, 2026
8e5908a
http(upgrade): fix double-request on req.end(body) + non-101 leak + d…
robobun Apr 26, 2026
25dffeb
test(29012): drop setTimeout-based wait in favor of second-connection…
robobun Apr 26, 2026
abe6788
http(upgrade): rename inner isUpgrade, un-flake req.end(body) test
robobun Apr 26, 2026
edf8944
http(upgrade): use captured outer isUpgrade in leak-fix guard
robobun Apr 26, 2026
f97c969
http(upgrade): cache hasUpgradeHeaders() result once per request
robobun Apr 27, 2026
5a4987a
http(upgrade): use ERR_STREAM_DESTROYED; gate has_framing scan on upg…
robobun Apr 27, 2026
8317f2b
http(upgrade): document head=Buffer.alloc(0) divergence from Node.js
robobun May 4, 2026
b66c1e6
http(upgrade): drain kBodyChunks after generator exit
robobun May 4, 2026
1d36c13
ci: retrigger
robobun May 11, 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
54 changes: 51 additions & 3 deletions src/http/http.zig
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,12 @@ pub const Flags = packed struct(u32) {
/// Set after the first H3 retry so a stale-session/GOAWAY race retries
/// once on a fresh connection but never loops.
h3_retried: bool = false,
_: u13 = 0,
/// Set by `buildRequest()` if the request on the wire carries explicit
/// body framing (either `Transfer-Encoding: chunked` that survived header
/// truncation, or `Content-Length`). Used by `writeToStream()` to decide
/// whether to drain the streaming request body before the 101 response.
request_body_has_framing: bool = false,
_: u12 = 0,
};

// TODO: reduce the size of this struct
Expand Down Expand Up @@ -1082,6 +1087,40 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
header_count += 1;
}

// Compute `request_body_has_framing` from the *finalized* header slice
// that will hit the wire — not from the raw user headers, which may
// include entries past `max_user_headers` that have been dropped, or a
// `Transfer-Encoding` value that is not `chunked`.
//
// Used by `writeToStream()` (below) to decide whether to drain a
// streaming request body buffer while the upgrade is pending.
// `FetchTasklet.skipChunkedFraming()` reads the user headers directly
// and does NOT consult this flag — the two code paths deliberately ask
// different questions (drain vs. chunk-wrap).
//
// Only upgrade requests read this flag (the consumer in
// `writeToStream()` short-circuits on `upgrade_state == .pending`),
// so skip the scan entirely for the >99% non-upgrade case.
// `upgrade_state` is set by the first header loop above, so it's
// authoritative by the time we get here.
if (this.flags.upgrade_state != .none) {
var has_framing = false;
for (request_headers_buf[0..header_count]) |h| {
const hash = hashHeaderName(h.name);
if (hash == hashHeaderConst(content_length_header_name)) {
has_framing = true;
break;
}
if (hash == hashHeaderConst(chunked_encoded_header.name) and
bun.strings.containsCaseInsensitiveASCII(h.value, "chunked"))
{
has_framing = true;
break;
}
}
this.flags.request_body_has_framing = has_framing;
}

return picohttp.Request{
.method = @tagName(this.method),
.path = this.url.pathname,
Expand Down Expand Up @@ -1554,8 +1593,17 @@ pub fn writeToStream(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPCo
}
var stream = &this.state.original_request_body.stream;
const stream_buffer = stream.buffer orelse return;
if (this.flags.upgrade_state == .pending) {
// cannot drain yet, upgrade is waiting for upgrade
if (this.flags.upgrade_state == .pending and !this.flags.request_body_has_framing) {
// For upgrade requests with no explicit body framing (WebSocket-style),
// writes represent post-upgrade protocol data and must be held until
// the server responds 101. For requests that *do* have explicit framing
// (e.g. dockerode sends `Transfer-Encoding: chunked` with an `Upgrade:`
// header), the body is part of the HTTP request and must be drained so
// the server can parse it before deciding to switch protocols.
//
// `request_body_has_framing` is set by `buildRequest()` from the header
// set that actually makes it to the wire, so a late/dropped
// `Transfer-Encoding` user header will not falsely enable draining.
return;
}
const buffer = stream_buffer.acquire();
Expand Down
Loading
Loading