RSC payload served from browser disk cache after router cache is cleared by a mutation #97485
Replies: 8 comments 2 replies
|
I've been able to repro this, let me do a bit more of triaging, I'll share it internally too. |
|
Likely cause: |
|
The difference in behavior comes down to how browser HTTP caching interacts with Next.js Client Router Cache versus Server Action redirects: 1. Why
|
|
Here is the breakdown of what is happening under the hood with Next.js 16, the browser disk cache, and why 1. Why
|
|
This looks less like a difference between updateTag() and revalidatePath() themselves, and more like an interaction between Next.js' router cache and the browser's HTTP cache for RSC responses. updateTag('todolist') invalidates the tagged server-side cache entry and also causes the client router cache to be cleared. But when you then call router.push('/todolist'), Chrome can satisfy the RSC GET from its own disk cache before that request ever reaches the Next.js server. That would explain the 200 OK (from disk cache) and why you end up putting stale RSC data back into the router cache. revalidatePath('/todolist') + redirect('/todolist') takes a different path through the Server Action lifecycle. The redirect is part of the action response, so Next.js can coordinate the revalidation/navigation as part of the same server-driven flow instead of relying on a later client router.push() request that may be intercepted by the browser cache. So I wouldn't assume updateTag and revalidatePath have fundamentally different cache invalidation semantics here. The important difference seems to be the navigation mechanism and the HTTP caching behavior of the RSC response. Given that disabling the browser cache or changing the Cache-Control header fixes the issue, that strongly suggests the stale layer is the browser HTTP cache, not the Next.js Data Cache itself. As a workaround, I would keep revalidatePath('/todolist') + redirect('/todolist') inside the Server Action for now, rather than doing updateTag() and then a client-side router.push(). |
|
This issue is caused by the interaction between two completely separate caching layers: the Next.js In-Memory Router Cache and the Browser’s Native HTTP Disk Cache (RFC 9111). When Here is the exact breakdown of what is happening under the hood and why: 1. Why
|
|
I think the key distinction here is between Next.js cache invalidation and the browser's HTTP cache.
That would explain why the behavior differs between:
In the first case, the subsequent navigation can issue a separate RSC request, and the browser may satisfy that request from its HTTP cache before it reaches Next.js. This also matches the observation that disabling the browser cache makes the issue disappear. I would therefore avoid relying on For now, using Since this has already been reproduced by the Next.js team, it looks more like a framework/cache-header interaction than an application-level invalidation mistake. |
|
Thanks for bringing this up in RSC payload served from browser disk cache after router cache is cleared by a mutation. I am following this discussion and looking into the details. Happy to help test or contribute to this area! |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Config:
The problem is:
Let's say I'm on a route '/todolist' which displays a todolist that includes a couple of todolist items, fetched from the database using a 'use cache' cached function which assigns a tag 'todolist' to this cache. I do a soft navigation from the '/todolist' to the '/todolist/[id]' route in order to update the todolist item via a server action, and then I do
updateTag('todolist')at the very end of it. I'm using a client-side form component, so I do a redirect back withrouter.push('/todolist')inside an event handler after a successfull mutation. It works exactly once, because after the first mutation the browser caches the previousrouter.push('/todolist')server 'rsc' response. So from now on I have this sequence of events, I guess:server action (success) ->
router.push('/todolist')call -> event handler is done running / urgent state updates are finished -> router cache is being purged due to theupdateTagcall (link prefetching starts, if any) -> soft-navigation to the '/todolist' route -> browser requests the '/todolists' page from the server checking its cache first, and finding the previously cached 'rsc' in there (I seeStatus Code 200 OK (from disk cache)for this request) -> 'rsc' is being stored in the router cache -> I see unupdated todolist after the navigation.What fixes this issue:
headers()/proxyfor this route when a 'rsc' request is commingrevalidatePath('/todolist')+redirect('/todolist')in the server action instead of the previous setup. I don't know why this works either, in this context callingrevalidatePathandupdateTagmust have no difference, and callingredirectfrom the server action also just makes a client-side navigation after the promise rejects in the event handler. The only difference that I spotted is that server response that happens after the navigation withrevalidatePath('/todolist')+redirect('/todolist')doesn't have a 'stale-while-revalidate' header. I'm not a huge browser cache expert, so I cannot tell if this is the root of the problem or notThe questions are:
updateTag+router.pushdoesn't work properly?updateTag+router.pushdifferent from therevalidatePath+redirectcombination?updateTag+router.pushwhen the browser gets a cached response from the disk cache? is it some soft of SWR but in the browser itself?All reactions