Skip to content

fix(http): default reason phrase; root the body chunk across a replaced writeHead - #13722

Merged
proggeramlug merged 1 commit into
mainfrom
fix/http-reason-phrase-first-burst
Nov 2, 2026
Merged

proggeramlug merged 1 commit into
mainfrom
fix/http-reason-phrase-first-burst

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Two perry node:http server bugs found by profiling compiled Next.js (secret-tests/nextjs-hello) against node.

A. Empty reason phrase

Next sets res.statusMessage = "" (send-response.js:22). On native responses nothing applied node's default (statusMessage ||= STATUS_CODES[statusCode] at head commit), so /api/hello answered HTTP/1.1 200 instead of HTTP/1.1 200 OK. The new commit_status_message in perry-ext-http/src/server/response.rs runs from writeHead and the implicit header. An explicit res.writeHead(200, "") keeps the empty reason, as node does.

B. Response body lost or truncated after a replaced writeHead (GC)

finalize_buffered_end (end) and js_node_http_res_write_with_cb (write) held the body chunk unrooted across implicit_header. When a program replaces res.writeHead (Next's compression / on-headers), that call runs JS, which can trigger a collection. The collection moved a young chunk, and the stale pointer was read afterwards. The client got Content-Length: 5130 and an empty or truncated body.

In Next this lost exactly one of the first 16 concurrent requests on every fresh server. It disappears once the cached page is promoted (around request 17). Plain node:http hits it too. The chunk is now rooted across the call at both sites.

Tests

  • test-files/test_gap_http_status_message_default.ts: fixed == node; main differs.
  • test-files/test_gap_gc_http_body_chunk_across_write_head.ts: fixed == node; main sends bodies of 0, 2 or 7 bytes instead of 6290 and 7090.

Verification (qb9; Next checks on this branch + #13720 merged for /list)

Check Result
both tests fixed == node, main differs
writeHead(200, "") / statusMessage = "" / statusCode = 404; end() == node
nextjs-hello fresh start, 16 concurrent /, 10 starts 10/10, every request answered with a body (was 1 lost per start)
/api/hello status line HTTP/1.1 200 OK == node
5 routes (body + headers except date) identical to node
load, 5000 requests, 16 connections, no warm-up / 852 req/s, 0 errors; /api/hello 727 req/s, 0 errors
perry-ext-http unit tests 217 passed / 1 failed, same set as main (chunked_trailers_are_buffered_and_delivered_together); two client_turnloop::native_client tests abort on main too
rustfmt clean

Not audited here: other JS values held across JS-running native calls elsewhere in ext-http.

…te chunk across implicit_header

Two causes in perry-ext-http's server response:

1. A statusMessage of "" (Next's send-response copies Response.statusText,
   which is "") was never defaulted at head commit, so the wire got
   "HTTP/1.1 200 ". The native writeHead and implicit_header now apply node's
   statusMessage ||= STATUS_CODES[code] || 'unknown' (ResponseState::commit_status_message),
   and writeHead(code, "") keeps an explicit empty reason as node does.

2. finalize_buffered_end and js_node_http_res_write_with_cb read the body
   chunk after implicit_header, which can run a program-replaced writeHead
   and a GC. A moved young chunk left a forwarding pointer, so the body was
   read as empty or truncated while Content-Length was already sent and the
   client hung (first reused keep-alive request on a fresh Next server).
   The chunk is now rooted before implicit_header and read back after it.
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@proggeramlug
proggeramlug marked this pull request as ready for review November 2, 2026 05:38
@proggeramlug
proggeramlug merged commit b85bf30 into main Nov 2, 2026
119 of 136 checks passed
@proggeramlug
proggeramlug deleted the fix/http-reason-phrase-first-burst branch November 2, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant