cookies() inside after() returns pre-mutation values in a Route Handler
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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