Skip to content

fix(cookies): prevent mutation outside action phase#91069

Open
Neiland85 wants to merge 1 commit intovercel:canaryfrom
Neiland85:neiland85/cookie-mutation-phase-guard
Open

fix(cookies): prevent mutation outside action phase#91069
Neiland85 wants to merge 1 commit intovercel:canaryfrom
Neiland85:neiland85/cookie-mutation-phase-guard

Conversation

@Neiland85
Copy link

@Neiland85 Neiland85 commented Mar 8, 2026

Summary

Adds a runtime guard preventing cookie mutations outside the action phase.

Context

cookies().set() and cookies().delete() currently rely on callers respecting
the mutation phase. This change ensures mutations are validated against the
RequestStore.phase at runtime before executing.

Behavior

If a mutation is attempted outside the allowed phase (action), the call
throws ReadonlyRequestCookiesError.

Impact

  • Prevents invalid cookie mutations during render/after phases
  • Aligns runtime behavior with server action semantics
  • Minimal runtime overhead

Copilot AI review requested due to automatic review settings March 8, 2026 18:59
@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: 7ee9d17

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts cookie mutation behavior in the server request cookie adapters to ensure cookies cannot be mutated outside the Server Action (“action”) phase.

Changes:

  • Adds phase checks to MutableRequestCookiesAdapter.wrap() to block set/delete when cookies are no longer mutable.
  • Refactors/moves appendMutableCookies within the module (no intended functional change).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +112 to +118
const requestStore = workAsyncStorage.getStore() as
| RequestStore
| undefined

if (requestStore) {
ensureCookiesAreStillMutable(requestStore, 'cookies().delete')
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workAsyncStorage.getStore() is typed to WorkStore (app-render work store), not RequestStore. Casting it to RequestStore and passing it into ensureCookiesAreStillMutable will treat the work store as if it has a phase field, causing cookies().set/delete to throw unexpectedly whenever a WorkStore is present (because phase will be undefined). This should read from workUnitAsyncStorage.getStore() (and then narrow to type === 'request') or otherwise obtain the actual RequestStore that owns the phase transitions.

Copilot uses AI. Check for mistakes.
Comment on lines 132 to +140
case 'set':
return function (...args: SetCookieArgs) {
const requestStore = workAsyncStorage.getStore() as
| RequestStore
| undefined

if (requestStore) {
ensureCookiesAreStillMutable(requestStore, 'cookies().set')
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces phase-dependent mutation checks inside MutableRequestCookiesAdapter.wrap, but there are no unit tests covering the new behavior (e.g., that mutations are allowed in phase === 'action' and rejected after transitioning to render/after). Consider adding tests similar to the existing wrapWithMutableAccessCheck suite to prevent regressions once the correct async storage is used.

Copilot uses AI. Check for mistakes.
// headers have been set.
case 'delete':
return function (...args: [string] | [ResponseCookie]) {
const requestStore = workAsyncStorage.getStore() as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MutableRequestCookiesAdapter.wrap incorrectly uses workAsyncStorage.getStore() (which returns a WorkStore) and casts it to RequestStore to check phase, but WorkStore has no phase property, causing ensureCookiesAreStillMutable to always throw ReadonlyRequestCookiesError.

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants