feat(runtime): node:http/https streaming response bodies end-to-end + client/server parity fixes#5057
Merged
Merged
Conversation
added 4 commits
June 13, 2026 06:47
…/write→wire, client head/chunk/end events) - server: HyperResponseShape body becomes Full|Stream; flushHeaders/first write flush the head with a channel-backed hyper body (chunked unless Content-Length set); end() sends final chunk + trailers and closes; single-shot end(body) keeps the buffered Content-Length path - backpressure: in-flight byte counter, write() returns false past 16KiB, pump emits 'drain' when it sinks below the HWM - reaper: streaming responses aren't synthesized over; dead body receiver counts as peer-gone - client: reqwest reads stream via chunk(); new ResponseHead/Chunk/End events deliver headers before the body completes; data buffered until listeners attach, flushed at end - client req.flushHeaders() dispatches body-less requests immediately (new js_http_client_request_flush_headers static route + twins) - req.httpVersion reflects the wire version (1.0/1.1/2.0) + new httpVersionMajor/Minor dispatch properties
…essage reason-phrase, Node-default client headers - req.abort() emits 'abort'+'close' once and suppresses any later end() dispatch (server must never see the request) - httpVersionMajor/Minor wired through HIR member lowering, codegen static table, and the dynamic dispatch tower (dot + computed access) - custom res.statusMessage reaches the HTTP/1 status line via hyper's ReasonPhrase extension - client: drop the perry/<version> default User-Agent (Node sends none) and send Node's default 'Connection: keep-alive'
…FFI decls to stdlib_ffi_part2 (2000-line lint gate)
…t-consistency gate) + regen api docs
6e09f48 to
7b270e8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Working the node:http/http2/https parity mandate. The in-repo vectors turned out to be already green on main (node-suite http 18/18, http2 9/9, https 5/5;
test_parity_http*inventory tests diff-clean vs node v26) — the mandate's "5/18, 13 crashes" numbers were stale. The real gap is against Node's own test/parallel corpus (scripts/node_core_subset.pyradar): http 30%, https 20%, http2 7% at baseline, with 186 of 403 runtime failures being silent hangs.Root cause of the silent-hang class: nothing streamed
HyperResponseShapeand only handed to hyper via a oneshot atres.end().res.flushHeaders()set a flag and sent nothing;res.write()buffered. Any client waiting on headers/chunks before response completion waited forever.dispatch_requestdidresponse.bytes().await— the'response'callback only fired after the complete body arrived.Changes
Server streaming (
perry-ext-http-server/response.rs,server.rs):HyperResponseShape.bodyis nowShapeBody::Full | ShapeBody::Stream(channel-backed hyperBodyimpl,Frame::data+Frame::trailers).res.flushHeaders()/ firstres.write()flush the head to the wire immediately (chunked unless Content-Length set — Node's wire behavior);res.end()sends the final chunk + trailers and closes the channel. Single-shotres.end(body)keeps the buffered Content-Length path byte-for-byte.write()returnsfalsepast the 16 KiB HWM; the pump emits'drain'when it sinks below. Reaper treats a dead stream receiver as peer-gone and never synthesizes over a streaming response.res.statusMessagenow reaches the HTTP/1 status line via hyper'sReasonPhraseextension.req.httpVersionreflects the wire version (1.0/1.1/2.0); newhttpVersionMajor/httpVersionMinor(HIR member lowering + codegen static table + dynamic dispatch tower — the dot-access path previously hit the pre-tagged-receiver 0.0 sentinel).Client streaming (
perry-ext-http/client_dispatch.rs,client_events.rs,lib.rs):ResponseHead/ResponseChunk/ResponseEndpump events: the response callback fires when headers arrive; body chunks deliver as'data'events as they come off the socket (buffered until listeners attach, flushed at'end').req.flushHeaders()dispatches body-less requests immediately (newjs_http_client_request_flush_headersstatic route + stdlib twin); a laterend()drains the write/finish/end callback ordering exactly once.req.abort()beforeend()now tears down properly: emits'abort'+ once-only'close', suppresses dispatch (server never sees the request), no spurious'error'.perry/<version>default User-Agent (Node sends none) and send Node's defaultConnection: keep-alive— aligns server-side header observations with Node.Results
Node-core radar (Node v22 own tests,
--auto-optimize):Corpus tests fixed:
test-http-flush-headers,test-http-flush-response-headers,test-http-client-response-timeout,test-http-abort-before-end,test-http-client-agent-abort-close-event.¹ the two "new" failures are verified flakes, not regressions:
test-https-agent-snihangs identically on the pre-change binary (TLS cert-chain resolution, env-dependent);test-http-dns-errorpasses on both builds when run idle (DNS/load flake during the sweep).The modest pass-count delta understates the change: most remaining corpus failures sit behind additional blockers (client
'readable'/read()pull API,Expect: 100-continue, a real client-socket surface, Agent pool internals, keep-alive wire enforcement). The streaming foundation is what SSE / long-poll / chunked responses — i.e. real Express/Fastify/Hono workloads — need, and those follow-ups now have somewhere to land.Regression guard
origin/mainbuild — every one fails there too (0 regressions).cargo test -p perry-ext-http -p perry-ext-http-servergreen;cargo fmt --allclean; no Cargo.toml-version / CLAUDE.md / CHANGELOG edits (maintainer folds metadata at merge).Files
perry-ext-http-server/{response,server,request,handle_dispatch,http2_server}.rs,perry-ext-http/{lib,client_dispatch,client_events,client_request_surface,tls_client,agent,tests}.rs,perry-stdlib/src/{common/dispatch.rs,http/client_request_surface.rs},perry-codegen/src/{ext_registry.rs,runtime_decls/stdlib_ffi.rs,lower_call/native_table/{http_client,http_server}.rs},perry-hir/src/lower/expr_member.rs