You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Proposal: let apps intervene before the RSC clientVersion skew-recovery reload
#15343
We've adopted the 8.3 clientVersion stale-client detection across four RSC Framework Mode apps (custom entries) and it works well. This proposal is about the one place it currently limits us: the recovery reload is unconditional and invisible to the app, and for a collaborative app that combination is actively harmful. Following the contributing guide, here's how we're limited by the current API first; a candidate shape is at the end.
The limitation
On a detected mismatch the runtime does window.location.href = errorReloadPath (guarded by sessionStorage) with no way for the app to know it's happening. Two real consequences we hit:
1. beforeunload guards fire on the framework's own recovery
Collaborative editors guard against losing unsynced changes:
When the skew reload fires mid-session, the browser shows the native Leave site? dialog in the middle of the framework's automatic recovery. If the user cancels it:
the reload is aborted, but the sessionStorage guard for that version is already armed, so subsequent mismatching fetches short-circuit into never-resolving promises β the app is permanently stale and hung, with no signal to anyone;
the app cannot distinguish this reload from an ordinary navigation, so it can't suppress its own guard. There is no existing signal (event, flag, callback) that says "this unload is a version-skew recovery, let it through."
2. Session telemetry dies with the page
Error/analytics buffers flush on a best-effort pagehide, but a hard location.href assignment mid-fetch loses whatever context explains why the reload happened. We'd like to record "reloaded due to version skew, from version X" β today the only trace is a console warning.
The workaround we currently maintain
Wrapping the fetch passed to the RSC entry points and re-implementing detection + bounded recovery end-to-end (~400 lines per app: version header, mismatch classification, history-state loop guard, a window flag our collab provider's beforeunload guard checks, telemetry flush). It predates 8.3 β but 8.3's built-in can't replace it solely because there's no seam at the moment of recovery. We'd much rather delete our pipeline and keep the framework's.
Candidate shape (one option β genuinely open to different ones)
Awaited immediately before the automatic reload. Apps flush telemetry / set an "expected reload" flag, return nothing, and the framework proceeds β byte-identical behavior when unregistered.
return false = the app owns recovery: the runtime still refuses to apply the stale payload, but skips its own location.href. In that case the sessionStorage guard should arguably not be armed β otherwise the second mismatch for the same version short-circuits before the hook and later fetches hang silently. Pinning that interaction down is part of the design.
Not invoked for the deferred eager-discovery case (no reload happens there today).
Open design points we'd like maintainer input on
Registration surface. An RSCHydratedRouter prop cannot reach the createCallServer path (it's wired via setServerCallback independently), so a prop-based API can't service server-action mismatches. A registration on the shared surface next to the existing router globals would cover all call sites β including, if desired, the non-RSC fog-of-war path. Alternatively RSC-scope it and drop "server-action".
Loop-guard semantics under app ownership (see above).
Whether this should wait for the broader clientVersion API to stabilize, and if so, what interim escape hatch you'd suggest for the beforeunload collision β that one bites today.
Related: while evaluating this we found that createCallServer applies action rerender payloads without a version check β filed separately as a straightforward coverage fix, since it adds no API surface: #15342
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
We've adopted the 8.3
clientVersionstale-client detection across four RSC Framework Mode apps (custom entries) and it works well. This proposal is about the one place it currently limits us: the recovery reload is unconditional and invisible to the app, and for a collaborative app that combination is actively harmful. Following the contributing guide, here's how we're limited by the current API first; a candidate shape is at the end.The limitation
On a detected mismatch the runtime does
window.location.href = errorReloadPath(guarded by sessionStorage) with no way for the app to know it's happening. Two real consequences we hit:1.
beforeunloadguards fire on the framework's own recoveryCollaborative editors guard against losing unsynced changes:
When the skew reload fires mid-session, the browser shows the native Leave site? dialog in the middle of the framework's automatic recovery. If the user cancels it:
2. Session telemetry dies with the page
Error/analytics buffers flush on a best-effort
pagehide, but a hardlocation.hrefassignment mid-fetch loses whatever context explains why the reload happened. We'd like to record "reloaded due to version skew, from version X" β today the only trace is a console warning.The workaround we currently maintain
Wrapping the
fetchpassed to the RSC entry points and re-implementing detection + bounded recovery end-to-end (~400 lines per app: version header, mismatch classification, history-state loop guard, a window flag our collab provider'sbeforeunloadguard checks, telemetry flush). It predates 8.3 β but 8.3's built-in can't replace it solely because there's no seam at the moment of recovery. We'd much rather delete our pipeline and keep the framework's.Candidate shape (one option β genuinely open to different ones)
return false= the app owns recovery: the runtime still refuses to apply the stale payload, but skips its ownlocation.href. In that case the sessionStorage guard should arguably not be armed β otherwise the second mismatch for the same version short-circuits before the hook and later fetches hang silently. Pinning that interaction down is part of the design.Open design points we'd like maintainer input on
RSCHydratedRouterprop cannot reach thecreateCallServerpath (it's wired viasetServerCallbackindependently), so a prop-based API can't service server-action mismatches. A registration on the shared surface next to the existing router globals would cover all call sites β including, if desired, the non-RSC fog-of-war path. Alternatively RSC-scope it and drop"server-action".clientVersionAPI to stabilize, and if so, what interim escape hatch you'd suggest for thebeforeunloadcollision β that one bites today.Related: while evaluating this we found that
createCallServerapplies action rerender payloads without a version check β filed separately as a straightforward coverage fix, since it adds no API surface: #15342Written on behalf of agcty.
All reactions