[PR #402] [MERGED] fix: next/headers readonly semantics and legacy sync compatibility #545

Closed
opened 2026-05-06 13:08:41 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/402
Author: @JaredStowell
Created: 3/10/2026
Status: Merged
Merged: 3/10/2026
Merged by: @james-elicx

Base: mainHead: jstowell/fix-request-api-readonly-compat


📝 Commits (8)

  • 1657d7e Add headers/cookies legacy tests
  • a76d206 Fix headers sync error handling
  • 11ea525 Fix headers sync guards
  • 5ec93e5 bonk
  • 4bdd5e3 Update packages/vinext/src/shims/headers.ts
  • 4a5ad63 bonk
  • b37311e Clarify dynamic error comment
  • 66de12d Merge remote-tracking branch 'origin/main' into jstowell/fix-request-api-readonly-compat

📊 Changes

16 files changed (+1224 additions, -374 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+44 -43)
📝 packages/vinext/src/shims/headers.ts (+225 -12)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+264 -258)
📝 tests/e2e/app-router/server-actions.spec.ts (+36 -0)
tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/actions.ts (+8 -0)
tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/page.tsx (+29 -0)
tests/fixtures/app-basic/app/nextjs-compat/api/headers-readonly/route.ts (+18 -0)
tests/fixtures/app-basic/app/nextjs-compat/api/request-api-repeat/route.ts (+29 -0)
tests/fixtures/app-basic/app/nextjs-compat/api/request-api-sync/route.ts (+17 -0)
tests/fixtures/app-basic/app/nextjs-compat/draft-mode-dynamic-error/page.tsx (+21 -0)
tests/fixtures/app-basic/app/nextjs-compat/request-api-dynamic-error/page.tsx (+32 -0)
tests/fixtures/app-basic/app/nextjs-compat/request-api-readonly/page.tsx (+33 -0)
tests/fixtures/app-basic/app/nextjs-compat/request-api-sync/page.tsx (+15 -0)
📝 tests/nextjs-compat/draft-mode.test.ts (+10 -0)
tests/nextjs-compat/request-apis.test.ts (+117 -0)
📝 tests/shims.test.ts (+326 -61)

📄 Description

Fix next/headers compatibility gaps so vinext matches Next.js more closely for request APIs.

This changes headers() and cookies() to support the legacy sync access pattern while preserving their Promise-based API, makes headers() read-only, and restricts cookies() mutation to route handlers and server actions instead of allowing writes during normal render paths.

What changed

  • Added decorated Promise return values for headers() and cookies() so legacy sync access like headers().get(...) and cookies().get(...) works.
  • Made the public headers() result read-only and throw on set, append, and delete.
  • Split cookie behavior by phase:
    • render: read-only
    • route handler: mutable
    • server action: mutable
  • Added phase tracking to the next/headers shim state.
  • Updated the App Router runtime to switch request API phase around route handlers and server actions.
  • Kept the internal mutable request header object for framework use, while sealing the public user-facing API.

Tests

Added substantial regression coverage across unit, integration, and browser layers.

Unit / shim coverage

Expanded tests/shims.test.ts to cover:

  • legacy sync access for headers() and cookies()
  • read-only headers() for sync and awaited access
  • read-only cookies() during render
  • mutable cookies() only in route-handler and action phases
  • mutable cookie references becoming read-only again after phase changes
  • validation behavior for writable cookie paths in route-handler phase

HTTP integration coverage

Added tests/nextjs-compat/request-apis.test.ts and new fixture routes/pages to verify:

  • sync headers() and cookies() access in real App Router page requests
  • render-path readonly behavior for both APIs
  • route-handler mutability for cookies()
  • route-handler readonly behavior for headers()
  • mixed sync and awaited access within the same route handler

New fixtures:

  • tests/fixtures/app-basic/app/nextjs-compat/request-api-sync/page.tsx
  • tests/fixtures/app-basic/app/nextjs-compat/request-api-readonly/page.tsx
  • tests/fixtures/app-basic/app/nextjs-compat/api/request-api-sync/route.ts
  • tests/fixtures/app-basic/app/nextjs-compat/api/headers-readonly/route.ts
  • tests/fixtures/app-basic/app/nextjs-compat/api/request-api-repeat/route.ts

Browser / server-action coverage

Added a Playwright regression in tests/e2e/app-router/server-actions.spec.ts plus a new fixture page/action:

  • tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/page.tsx
  • tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/actions.ts

This verifies that:

  • a server action can set cookies successfully
  • the subsequent render path still treats cookies() as read-only
  • the cookie persists and is visible on reload

Validation

Ran:

  • pnpm test tests/shims.test.ts tests/nextjs-compat/request-apis.test.ts tests/nextjs-compat/set-cookies.test.ts tests/nextjs-compat/draft-mode.test.ts
  • pnpm test tests/nextjs-compat/request-apis.test.ts
  • PLAYWRIGHT_PROJECT=app-router pnpm run test:e2e tests/e2e/app-router/server-actions.spec.ts
  • pnpm run typecheck
  • pnpm run lint

Notes

This preserves vinext’s internal lazy mutable request header optimization for middleware forwarding, but seals the public API so user code can no longer mutate request headers or write cookies from normal render contexts.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/402 **Author:** [@JaredStowell](https://github.com/JaredStowell) **Created:** 3/10/2026 **Status:** ✅ Merged **Merged:** 3/10/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `jstowell/fix-request-api-readonly-compat` --- ### 📝 Commits (8) - [`1657d7e`](https://github.com/cloudflare/vinext/commit/1657d7e3b445012d49c40a22207fd880b1f1630d) Add headers/cookies legacy tests - [`a76d206`](https://github.com/cloudflare/vinext/commit/a76d2065b530df7b7055400d6c96b34a1bb85ce9) Fix headers sync error handling - [`11ea525`](https://github.com/cloudflare/vinext/commit/11ea52548ef4f4649f18a58ad3f8c266bf9515e7) Fix headers sync guards - [`5ec93e5`](https://github.com/cloudflare/vinext/commit/5ec93e5d345765de7398b0bd00786e59a627e237) bonk - [`4bdd5e3`](https://github.com/cloudflare/vinext/commit/4bdd5e30e86057a3011f0f0375b2f634708721dc) Update packages/vinext/src/shims/headers.ts - [`4a5ad63`](https://github.com/cloudflare/vinext/commit/4a5ad63b67e93128ff0f37b11918ab555130f70b) bonk - [`b37311e`](https://github.com/cloudflare/vinext/commit/b37311e0ad004494a8b6f9d7a2bcdb5ff377bc59) Clarify dynamic error comment - [`66de12d`](https://github.com/cloudflare/vinext/commit/66de12d6664a5cfdcd20c3adffbcea121bb44c99) Merge remote-tracking branch 'origin/main' into jstowell/fix-request-api-readonly-compat ### 📊 Changes **16 files changed** (+1224 additions, -374 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+44 -43) 📝 `packages/vinext/src/shims/headers.ts` (+225 -12) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+264 -258) 📝 `tests/e2e/app-router/server-actions.spec.ts` (+36 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/actions.ts` (+8 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/page.tsx` (+29 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/api/headers-readonly/route.ts` (+18 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/api/request-api-repeat/route.ts` (+29 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/api/request-api-sync/route.ts` (+17 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/draft-mode-dynamic-error/page.tsx` (+21 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/request-api-dynamic-error/page.tsx` (+32 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/request-api-readonly/page.tsx` (+33 -0) ➕ `tests/fixtures/app-basic/app/nextjs-compat/request-api-sync/page.tsx` (+15 -0) 📝 `tests/nextjs-compat/draft-mode.test.ts` (+10 -0) ➕ `tests/nextjs-compat/request-apis.test.ts` (+117 -0) 📝 `tests/shims.test.ts` (+326 -61) </details> ### 📄 Description Fix `next/headers` compatibility gaps so vinext matches Next.js more closely for request APIs. This changes `headers()` and `cookies()` to support the legacy sync access pattern while preserving their Promise-based API, makes `headers()` read-only, and restricts `cookies()` mutation to route handlers and server actions instead of allowing writes during normal render paths. ## What changed - Added decorated Promise return values for `headers()` and `cookies()` so legacy sync access like `headers().get(...)` and `cookies().get(...)` works. - Made the public `headers()` result read-only and throw on `set`, `append`, and `delete`. - Split cookie behavior by phase: - render: read-only - route handler: mutable - server action: mutable - Added phase tracking to the `next/headers` shim state. - Updated the App Router runtime to switch request API phase around route handlers and server actions. - Kept the internal mutable request header object for framework use, while sealing the public user-facing API. ## Tests Added substantial regression coverage across unit, integration, and browser layers. ### Unit / shim coverage Expanded `tests/shims.test.ts` to cover: - legacy sync access for `headers()` and `cookies()` - read-only `headers()` for sync and awaited access - read-only `cookies()` during render - mutable `cookies()` only in route-handler and action phases - mutable cookie references becoming read-only again after phase changes - validation behavior for writable cookie paths in route-handler phase ### HTTP integration coverage Added `tests/nextjs-compat/request-apis.test.ts` and new fixture routes/pages to verify: - sync `headers()` and `cookies()` access in real App Router page requests - render-path readonly behavior for both APIs - route-handler mutability for `cookies()` - route-handler readonly behavior for `headers()` - mixed sync and awaited access within the same route handler New fixtures: - `tests/fixtures/app-basic/app/nextjs-compat/request-api-sync/page.tsx` - `tests/fixtures/app-basic/app/nextjs-compat/request-api-readonly/page.tsx` - `tests/fixtures/app-basic/app/nextjs-compat/api/request-api-sync/route.ts` - `tests/fixtures/app-basic/app/nextjs-compat/api/headers-readonly/route.ts` - `tests/fixtures/app-basic/app/nextjs-compat/api/request-api-repeat/route.ts` ### Browser / server-action coverage Added a Playwright regression in `tests/e2e/app-router/server-actions.spec.ts` plus a new fixture page/action: - `tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/page.tsx` - `tests/fixtures/app-basic/app/nextjs-compat/action-cookie-phase/actions.ts` This verifies that: - a server action can set cookies successfully - the subsequent render path still treats `cookies()` as read-only - the cookie persists and is visible on reload ## Validation Ran: - `pnpm test tests/shims.test.ts tests/nextjs-compat/request-apis.test.ts tests/nextjs-compat/set-cookies.test.ts tests/nextjs-compat/draft-mode.test.ts` - `pnpm test tests/nextjs-compat/request-apis.test.ts` - `PLAYWRIGHT_PROJECT=app-router pnpm run test:e2e tests/e2e/app-router/server-actions.spec.ts` - `pnpm run typecheck` - `pnpm run lint` ## Notes This preserves vinext’s internal lazy mutable request header optimization for middleware forwarding, but seals the public API so user code can no longer mutate request headers or write cookies from normal render contexts. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:41 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#545
No description provided.