vercel / vercel/next.js

cookies() inside after() returns pre-mutation values in a Route Handler

Open
#98,222 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Route Handlers Runtime
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/Nixxx19/nextjs-after-cookies-repro

To Reproduce

./repro.sh in the linked repo. It builds a one-route app and runs it with next start.

The route handler reads cookies(), sets session=NEW, reads it again, and then reads it once more inside after():

export async function GET() {
  const store = await cookies()
  const inbound = store.get('session')?.value ?? '<none>'

  store.set('session', 'NEW')
  const inHandler = (await cookies()).get('session')?.value ?? '<none>'

  after(async () => {
    const inAfter = (await cookies()).get('session')?.value ?? '<none>'
    console.log(`AFTER_READ inbound=${inbound} inHandler=${inHandler} inAfter=${inAfter}`)
  })

  return Response.json({ inbound, inHandler })
}

Send a request carrying Cookie: session=OLD.

Current vs. Expected behavior

Current:

response headers: set-cookie: session=NEW; Path=/
response body:    {"inbound":"OLD","inHandler":"NEW"}
AFTER_READ inbound=OLD inHandler=NEW inAfter=OLD

inAfter is OLD. The handler set the cookie, its own read returns NEW, and the response carries Set-Cookie: session=NEW, but the read inside after() sees the inbound jar.

Expected: inAfter is NEW, matching what the handler set and what the response sends.

This is the pattern the docs recommend. after.mdx, under "In Route Handlers and Server Functions", says you can call cookies and headers directly inside the after callback and that it "is useful for logging activity after a mutation", with an example that performs a mutation and then logs (await cookies()).get('session-id') from inside after(). That log records the pre-mutation value.

Provide environment information
Operating System:
  Platform: darwin
  Arch: arm64
  Available memory (MB): 24576
  Available CPU cores: 10
Binaries:
  Node: 25.9.0
  npm: 11.12.1
  pnpm: 10.33.2
Relevant Packages:
  next: 16.4.0-canary.15
  react: 19.2.8
  react-dom: 19.2.8
Which area(s) are affected? (Select all that apply)

Runtime, Route Handlers

Which stage(s) are affected? (Select all that apply)

next start (local)

Additional context

after() sets workUnitStore.phase = 'after' in server/after/after-context.ts. cookies() only returns the mutable jar while areCookiesMutableInCurrentPhase() holds, and that is requestStore.phase === 'action' (server/web/spec-extension/adapters/request-cookies.ts:220-222), so once the phase flips it falls back to the immutable inbound snapshot workUnitStore.cookies.

That snapshot is only reconciled by synchronizeMutableCookies (server/async-storage/request-store.ts), and its only caller is the Server Action path at server/app-render/action-handler.ts:1439. Route Handlers have no equivalent, so nothing reconciles the jar before after() runs.

Verified on 16.4.0-canary.15. I have not tried to fix it; resyncing before the phase flips, or calling synchronizeMutableCookies on the route-handler finalize path, look like the two obvious places, but which one is right is your call.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run ./repro.sh in the linked reproduction first to confirm the stale value. Then read server/after/after-context.ts, server/web/spec-extension/adapters/request-cookies.ts, server/async-storage/request-store.ts, and the route-handler finalization path, comparing it with server/app-render/action-handler.ts:1439. Done means the after() callback reads session=NEW after the handler mutation while preserving the response behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, next.js
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.