cloudflare / cloudflare/vinext

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

Open
#2,908 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## Upstream change

Next.js restored `headers()` so it reads through to the underlying incoming request instead of returning a detached snapshot.

- Commit: [`3c97df5`](https://github.com/vercel/next.js/commit/3c97df56ead9d1df81b36f891ba5ac0724c4eec0) "Restore the live `headers()` view of the incoming request" (#97166)
- fixes vercel/next.js#97049, closes vercel/next.js#97145

## Background

Between vercel/next.js#94703 and vercel/next.js#95116, `headers()` became detached from the request it describes:

- #94703 started stripping dev-only request-id headers alongside flight headers, but `HeadersAdapter.from` returns the `Headers` instance unchanged (no copy), so the deletes mutated the shared `req.headers`.
- #95116 fixed the mutation by **copying** the headers before stripping — but the copy detached the sealed view from the live request.

Result (since 16.3.0): a Proxy/middleware that writes a header onto `request.headers` and reads it back via `headers()` sees the value from *before* the write. Reading the same header directly off `request.headers` returns the new value, so the two APIs disagree. Whether a write is observed depends on ordering, because the view is built lazily on first access.

## Correct behavior (what to implement/verify)

1. `headers()` must be a **live view** of the incoming request — writes made to the request (e.g. by middleware/proxy) are reflected in `headers()`, regardless of ordering relative to the first `headers()` call.
2. A second `headers()` call must return the same live view (not a stale snapshot).
3. Internal/framework headers (flight headers, dev request-id headers) must stay **on the request** for framework plumbing, but stay **hidden from userland `headers()`** on read — even when written after the first `headers()` call. Hiding is done on *read* (via a sealed view that omits a set of header names), not by a one-time delete.
4. `forEach` on the sealed view must pass the **sealed proxy** as the `parent` argument, not the unsealed target (otherwise the callback gets a mutable handle that defeats the seal). `getSetCookie` must also respect the hidden set.
5. `cookies()` is unaffected — it stays a snapshot because `RequestCookies` parses the `cookie` header at construction time.

## Upstream test to port

`test/e2e/app-dir/proxy-headers-live-view/` — a proxy writes `set-during-proxy` onto `NextRequest.headers` and asserts via a `/probe` route:

```js
{
valueBeforeMutation: null,
valueOnRequest: 'set-during-proxy',
valueOnFirstView: 'set-during-proxy',
valueOnSecondView: 'set-during-proxy',
sameView: true,
internalHeaderOnRequest: 'set-during-proxy',
internalHeaderOnView: null,
internalHeaderIsIterated: false,
}
```

Also port the `HeadersAdapter` unit tests (`packages/next/src/server/web/spec-extension/adapters/headers.test.ts`, +130 lines) covering the omit-on-read seal, `forEach` parent, and `getSetCookie`.

## vinext relevance

vinext reimplements `next/headers`. We should verify our `headers()` shim reflects request mutations made by middleware and hides internal headers on read (not via one-time deletes), matching this parity fix. Check dev/prod parity across `entries/app-rsc-entry.ts`, `server/dev-server.ts`, `server/prod-server.ts`, and `cloudflare/worker-entry.ts`.

Contributor guide

Open the contributing guide

Research direction

Start with the `next/headers` shim and compare behavior across `entries/app-rsc-entry.ts`, `server/dev-server.ts`, `server/prod-server.ts`, and `cloudflare/worker-entry.ts`. Port the `proxy-headers-live-view` end-to-end test and the `packages/next/src/server/web/spec-extension/adapters/headers.test.ts` cases, then verify live request updates, hidden internal headers, the sealed `forEach` parent, and `getSetCookie` behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, typescript
Domain
api, backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.