Skip to content

fix(headers): expose live view of headers to api - #2912

Open
NriotHrreion wants to merge 1 commit into
cloudflare:mainfrom
NriotHrreion:fix/issue-2908-live-header-view
Open

fix(headers): expose live view of headers to api#2912
NriotHrreion wants to merge 1 commit into
cloudflare:mainfrom
NriotHrreion:fix/issue-2908-live-header-view

Conversation

@NriotHrreion

Copy link
Copy Markdown
Contributor

Closes #2908. Ports vercel/next.js#97166 (vercel/next.js@3c97df5)

Overview

Next.js restored headers() as a live, read-only view of the incoming request after a regression made it a detached snapshot. The snapshot caused ordering-dependent behavior: headers written by Proxy or middleware after the first headers() call were visible on request.headers but not through headers().

This PR aligns vinext with the restored nextjs semantics. Request header mutations remain observable through the same headers() view, framework-internal headers remain unavailable to userland, and cookies() retains its snapshot behavior.

What changed

  • No longer making a cleaned copy of the original Headers. Instead, return the sealed one.
function _getReadonlyHeaders(ctx: HeadersContext): Headers {
  if (!ctx.readonlyHeaders) {
-   const cleaned = new Headers(ctx.headers);
-   for (const header of FLIGHT_HEADERS) {
-     cleaned.delete(header);
-   }
-   cleaned.delete(NEXT_REQUEST_ID_HEADER);
-   cleaned.delete(NEXT_HTML_REQUEST_ID_HEADER);
-   ctx.readonlyHeaders = _sealHeaders(cleaned);
+   ctx.readonlyHeaders = _sealHeaders(ctx.headers, ...);
  }

  return ctx.readonlyHeaders;
}
  • _sealHeaders() and _createHidingHeaderMethods(): Wrap the original Headers, and proxy each method of it to dynamically hide the internal headers.

  • applyMiddlewareRequestHeaders():

    • Before: Directly set the ctx.headers to the upcoming new headers reference. So the old values still hold the old reference, not updated.
    • After: Use the new replaceHeadersContents() to loop through the upcoming headers and do updates on the original ctx.headers.
  • For cookies (rebuildCookiesFromHeader() and mergeMiddlewareSetCookies()):

    • Before: Loop through the upcoming cookies and do updates on the original ctx.cookies, merging old cookies and new cookies.
    • After: Directly set the ctx.cookies to the upcoming new cookies reference. So the old values still retain the old, not-updated reference, which aligns with the upstream nextjs's behavior.

headers() returns a live view of the real headers, dynamically proxied to hide the internal headers, while cookies() still returns a readonly copy of the real cookies.

Testing

  • pnpm test tests/shims.test.ts

@pkg-pr-new

pkg-pr-new Bot commented Aug 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2912
npm i https://pkg.pr.new/create-vinext-app@2912
npm i https://pkg.pr.new/@vinext/types@2912
npm i https://pkg.pr.new/vinext@2912

commit: 50db5d3

@github-actions

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 50db5d3 against base ee1d857 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 135.4 KB 135.4 KB ⚫ -0.0%
Client entry size (gzip) vinext 122.8 KB 122.8 KB ⚫ -0.0%
Dev server cold start vinext 2.85 s 2.82 s ⚫ -0.8%
Production build time vinext 2.92 s 2.94 s ⚫ +0.7%
RSC entry closure size (gzip) vinext 114.6 KB 114.8 KB ⚫ +0.1%
Server bundle size (gzip) vinext 194.8 KB 195.3 KB ⚫ +0.2%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@NriotHrreion

Copy link
Copy Markdown
Contributor Author

It seems the failed e2e test is not directly related to the changes of this PR

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.

next/headers: make headers() a live view of the incoming request (observe writes, hide internal headers on read)

1 participant