mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[PR #812] [MERGED] fix: invalidate cached headers/cookies snapshot in applyMiddlewareRequestHeaders #862
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#862
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/812
Author: @Shorebirdmgmt
Created: 4/10/2026
Status: ✅ Merged
Merged: 4/10/2026
Merged by: @james-elicx
Base:
main← Head:fix/invalidate-readonly-headers-after-middleware📝 Commits (1)
cff52acfix: invalidate cached headers/cookies snapshot in applyMiddlewareRequestHeaders📊 Changes
5 files changed (+185 additions, -2 deletions)
View changed files
📝
packages/vinext/src/shims/headers.ts(+18 -1)📝
tests/app-router.test.ts(+44 -0)➕
tests/fixtures/app-basic/app/header-override-after-prior-access/page.tsx(+33 -0)📝
tests/fixtures/app-basic/middleware.ts(+30 -1)📝
tests/shims.test.ts(+60 -0)📄 Description
Summary
applyMiddlewareRequestHeaders()rebuildsctx.headersin place (and, when the cookie header changes,ctx.cookies), but does not invalidate the sealed read-only snapshots cached on the sameHeadersContext. When a middleware readsnext/headers→headers()(orcookies()) before returning a request-header override viaNextResponse.next({ request: { headers } })orNextResponse.rewrite(..., { request: { headers } }), the sealed snapshot is built from the pre-override request, and every subsequentheaders()/cookies()call from the Server Component render returns that stale view. Middleware-injected request headers are missing from the render, and middleware-deleted credential headers are still visible.Any middleware using the common "read the request, then rewrite via `NextResponse.next({ request: { headers } })`" pattern is affected — authentication, logging/tracing, feature flagging, or any `next/headers`-aware pre-processing. The bug was discovered via `@clerk/nextjs`, whose `clerkClient()` reads `headers()` through its internal `buildRequestLike()` helper during middleware execution. Clerk's `auth()` in a Server Component then throws
because Clerk's own `x-clerk-auth-*` request header overrides never reach the render pipeline. That same mechanism is what unblocks `@clerk/nextjs` from `partial` (#803) to `supported` in `check.ts` as a downstream consequence of this fix — a follow-up PR will update the compat status once this one releases.
Root cause
`_getReadonlyHeaders(ctx)` caches `ctx.readonlyHeaders = _sealHeaders(ctx.headers)` on first access. When middleware reads `headers()` before returning, that cache is built from the original request headers. `applyMiddlewareRequestHeaders()` then replaces `ctx.headers` with the middleware-modified view, but the cached sealed snapshot is never invalidated. The Server Component's later `headers()` call returns the stale snapshot. The same race exists for `ctx.readonlyCookies` and `ctx.mutableCookies` when middleware mutates the cookie header.
Captured via `dist/` instrumentation against a production build of a minimal reproduction app (vinext@0.0.41 + @clerk/nextjs@7.0.12):
```
Error: trace
at _getReadonlyHeaders (dist/server/assets/headers-.js:424)
at headers (dist/server/assets/headers-.js:490)
at buildRequestLike (dist/server/index.js:1799) // @clerk/nextjs
at async clerkClient (dist/server/index.js:2591) // @clerk/nextjs
at async _handleRequest (dist/server/index.js:8635) // vinext RSC entry
```
The stack trace confirms Clerk's middleware-phase `headers()` call primes the sealed snapshot before vinext's own `applyMiddlewareRequestHeaders()` replaces the underlying `ctx.headers`.
Fix
`applyMiddlewareRequestHeaders()` now sets `ctx.readonlyHeaders = undefined` immediately after replacing `ctx.headers`, and clears `ctx.readonlyCookies` / `ctx.mutableCookies` in the cookie-rebuild branch. The next `headers()` / `cookies()` call rebuilds the sealed view from the post-override state.
Tests
Two new regression tests, both verified to fail on `main` and pass with the fix:
The integration test lives in the production-server describe block rather than the dev-server describe block because the dev-mode middleware path runs middleware before the headers context exists, so calling `headers()` from middleware is instead an immediate error there. The bug only manifests on the inline RSC entry path that matches production Cloudflare Workers.
Verified
With the fix applied:
Both new tests were also run with the fix reverted on a clean tree to confirm they deterministically catch the regression.
End-to-end runtime verification against a real `@clerk/nextjs@7.0.12` instance on a production `vinext build` + `vinext start`:
References
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.