Repository navigation
Version skew goes undetected on .data navigations to already-discovered routes in non-RSC framework mode
#15517
Replies: 2 comments
|
I went ahead and implemented this: #15518 (draft). It does what the proposal described. The server stamps its build version onto single fetch responses behind The integration test covers the case that started this: a tab navigating to an already-discovered route, getting The questions in this thread still stand, and the PR body adds a couple more that only came up while building it (header name, header vs payload, and whether this belongs in |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
In framework mode without RSC, a tab that's been open across a deploy can navigate to a route it already knows, fetch
.datafrom the new server, and hand that payload to the old client modules. Nothing compares the two builds.We'd like to add an opt-in check for this, and to write down what the expected behavior is either way. There's a draft PR at #15518.
What we saw
We run SSR on a single rolling deployment — old servers get replaced, not kept around. During a routine deploy, three people hit the root error boundary 17 times over six minutes. None of them had reloaded; they were clicking links.
/a/1→/a/2→/. All client navigations, so the tab keeps running A and only fetches.data..datarequests are answered by B.That release renamed a field in a loader payload (
oldField→newField). Our API kept both keys through the transition, but the server-side zod schema on B stripped the one it no longer declared, sooldFieldwasn't in the response. A's component readoldField.nameand threw.Reloading fixed it, and no new session hit it.
Why the existing check doesn't fire
Route discovery does compare build versions. It just only does that when it fetches the manifest, and it skips the fetch for paths the tab already knows:
All three of our hops were to already-discovered routes —
/a/:idwas the same route both times, and/came with the initial payload. SopatchRoutesOnNavigationreturned early, no manifest request went out, and nothing got compared.The
.datarequest carries no version of its own either.singleFetchUrl()builds<pathname>.datawith no version param, and we couldn't find aversionreference inpackages/react-router/lib/server-runtime/single-fetch.ts— the server readsX-Remix-Reload-Documenton redirects but never compares builds.RSC already does this
Since #15318 in 8.3.0, the RSC path compares the server's build version on every data fetch, not just during discovery.
getFetchAndDecodeViaRSCreads it off the payload and hands mismatches tohandleClientVersionMismatch. Route discovery doesn't come into it.Related: #14849 frames this gap as RSC-only. With #15318 shipped that looks reversed now, which is why this is a separate thread rather than a reply there.
What we'd build
The PR stamps the server's build version onto single-fetch responses behind
future.unstable_detectVersionSkewand compares it client-side, reusinghandleClientVersionMismatchand itssessionStorageloop guard. Redirects are returned before the comparison so an action redirect still reaches its target.We'd also want a section in
docs/start/framework/deploying.md. That page is a list of deployment templates right now, and whichever way this goes, what happens to open tabs across a deploy seems worth stating.Opt-in because the document navigation throws away in-flight form state, and if you keep previous builds addressable you never hit this.
What we'd like to know
Mostly whether the asymmetry is on purpose. If non-RSC mode is meant to tolerate skew on open sessions, the docs section alone is the right outcome and we'll drop the rest.
The other thing: is there a supported API for this that we missed? Before the PR we were patching
window.fetchinentry.clientto send our build version and checking it in a root loader, which works but leans onwindow.__reactRouterManifest.versionand the.dataURL convention, neither of which look public. We'd rather use something you already have.Environment
All reactions