Skip to content

fix(metadata): preserve Content-Length for fully buffered responses#2703

Open
tomvoss wants to merge 1 commit into
cloudflare:mainfrom
tomvoss:fix/metadata-content-length
Open

fix(metadata): preserve Content-Length for fully buffered responses#2703
tomvoss wants to merge 1 commit into
cloudflare:mainfrom
tomvoss:fix/metadata-content-length

Conversation

@tomvoss

@tomvoss tomvoss commented Jul 25, 2026

Copy link
Copy Markdown

Summary

closeAfterResponseWithBody() wraps every body-bearing App/Pages Router
response in a TransformStream so it can release deferred after() work
once the body closes. That wrap strips Content-Length, even for a response
vinext has already fully materialized in memory.

Metadata file convention responses (robots(), sitemap(), manifest(),
static icons) always serialize their body to a string or byte array before
returning — no producer is left that could still call after(). This marks
those responses (markFullyBufferedBody) and skips the wrap when nothing is
currently registered, letting the runtime emit an accurate Content-Length
instead of chunked transfer encoding.

Arbitrary Route Handler responses, and a metadata route's Response
passthrough (a user icon.tsx/opengraph-image.tsx can return a streaming
ImageResponse), are left unmarked — their body's producer can still call
after() after the handler resolves, so a "nothing registered yet" check
can't prove them safe. Those stay close-tracked.

Test plan

  • tests/after-response-close-worker.test.ts (real Cloudflare Workers
    runtime via wrangler): robots.txt and sitemap.xml regain
    Content-Length; a hand-written Route Handler stays chunked; a Route
    Handler returning new Response(stream) still runs a late after() call
    correctly after the stream completes or is cancelled.
  • tests/shims.test.ts: unit coverage for the marker gate, and protective
    tests proving a late after() call from an unmarked stream waits for
    completion/cancellation and connects to ExecutionContext.waitUntil.
  • Targeted and nearby suites (metadata routes, after()/dispatch, App/Pages
    Router prod and dev servers) pass. vp check passes. Both packages build.
  • The full local suite has pre-existing timeout and cross-filesystem-rename
    (EXDEV) failures in this sandbox, unrelated to this change and
    reproducing identically on unmodified main; not fully green.

Limitations

Hand-written Route Handler responses stay close-tracked even when their
current body happens to be buffered, because a returned ReadableStream
may keep producing and register after() after the handler function
resolves. Safely optimizing those would need a separate, principled
buffered-body signal.

closeAfterResponseWithBody() wraps every body-bearing response in a
TransformStream to support function-form after(), which strips
Content-Length even when the body is already fully materialized.
Metadata file convention responses (robots(), sitemap(), manifest(),
static icons) always serialize to a string or byte array with no
producer left, so mark those bodies (markFullyBufferedBody) and skip
the wrap when nothing is registered.

Arbitrary Route Handler responses and a metadata route's Response
passthrough stay wrapped: their body's producer may still call after()
after the handler resolves, so a "nothing registered yet" check can't
prove them safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqasUFjUPt2Q8gkrzrLGyB
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