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
Back/Forward to an entry the server now redirects is not followed: "Unprocessable popstate response" and an error boundary #1047
Back or Forward to an entry that has to be fetched, where the server now answers with a redirect, is not followed. The router logs "Unprocessable popstate response" and shows an error boundary.
Example: a session expires, the user presses Back to a protected page that is no longer in the history cache, and the server redirects to /login. A link to the same page follows the redirect; Back does not.
Where
packages/rangojs-router/src/browser/navigation-bridge.ts: the push path catches ServerRedirect and navigates to the target (around line 419). The popstate handler's catch has no such branch; it reaches console.error("[Browser] Unprocessable popstate response:", error) (around line 795).
Evidence
Found while working on #1031, in a unit probe through the bridge: a refetched traversal answered by a server redirect logged the message above and emitted an error-boundary update. Not yet reproduced in a browser.
Expected
The redirect is followed, as on a push: the router navigates to the redirect target, replacing the entry the browser moved to, so Back does not land on it again.
Test
Browser e2e in both apps, dev and production: a route whose handler redirects when a flag is set; visit it, navigate away far enough that its entry leaves the history cache (or with the cache cleared by an action), set the flag, press Back, and expect the redirect target's page with no error boundary.
Confirmed in a browser, dev and production, on the router test-app with the existing /auth-boundary/route-protected fixture (its route middleware redirects to /auth-boundary?rejected=route-mw unless the auth-boundary-token cookie is set).
Steps: set the cookie, navigate to the protected page, push 21 more entries so it leaves the 20-entry history cache, clear the cookie, go back 21.
dev: body "Internal Server Error ... Try Again Go to homepage"
console "[Browser] Unprocessable popstate response: ServerRedirect"
console "[RootErrorBoundary] Unhandled error caught: ServerRedirect"
production: the same error page; console "[Browser] Unprocessable popstate response: pe"
The address bar stays on the protected URL. Control in the same run: a link click to the same page follows the redirect and shows the target with an empty console.
What React Router does (quoted from packages/react-router/lib/router/router.ts)
It follows a redirect that a loader returns during a POP navigation. It does so as a PUSH: startRedirectNavigation uses NavigationType.Replace only when the original navigation was a replace or the response carries X-Remix-Replace, otherwise NavigationType.Push ("we just use the history action from the original navigation (PUSH or REPLACE)"), and a POP is neither. So with history [A, B (protected), C], Back from C to B redirects and the result is [A, B, login]; Back from login lands on B, which redirects again.
Proposed fix
In handlePopstate's catch (src/browser/navigation-bridge.ts), after the aborted/superseded return and before the location is set for the failure path: on ServerRedirect, validate the origin as the push path does and return this.navigate(redirectUrl, { state: error.state, replace: true, _skipCache: true }). About ten lines.
replace: true is the open choice. It turns the entry the browser moved to into the redirect target, which is what a browser does with a document that answers 302 during a traversal, and Back from the target then goes to the entry before the protected one. A push (React Router's choice) leaves the protected entry in history and produces the loop described above.
Tests: a shared body in both apps, dev and production (cloudflare-basic needs one cookie-gated redirect route), plus a unit case next to navigation-bridge-traversal-location.test.ts.
Activity
ivogt commentedon Oct 4, 2026
Confirmed in a browser, dev and production, on the router test-app with the existing
/auth-boundary/route-protectedfixture (its route middleware redirects to/auth-boundary?rejected=route-mwunless theauth-boundary-tokencookie is set).Steps: set the cookie, navigate to the protected page, push 21 more entries so it leaves the 20-entry history cache, clear the cookie, go back 21.
The address bar stays on the protected URL. Control in the same run: a link click to the same page follows the redirect and shows the target with an empty console.
What React Router does (quoted from
packages/react-router/lib/router/router.ts)It follows a redirect that a loader returns during a POP navigation. It does so as a PUSH:
startRedirectNavigationusesNavigationType.Replaceonly when the original navigation was a replace or the response carriesX-Remix-Replace, otherwiseNavigationType.Push("we just use the history action from the original navigation (PUSH or REPLACE)"), and a POP is neither. So with history[A, B (protected), C], Back from C to B redirects and the result is[A, B, login]; Back fromloginlands on B, which redirects again.Proposed fix
In
handlePopstate's catch (src/browser/navigation-bridge.ts), after the aborted/superseded return and before the location is set for the failure path: onServerRedirect, validate the origin as the push path does andreturn this.navigate(redirectUrl, { state: error.state, replace: true, _skipCache: true }). About ten lines.replace: trueis the open choice. It turns the entry the browser moved to into the redirect target, which is what a browser does with a document that answers 302 during a traversal, and Back from the target then goes to the entry before the protected one. A push (React Router's choice) leaves the protected entry in history and produces the loop described above.Tests: a shared body in both apps, dev and production (cloudflare-basic needs one cookie-gated redirect route), plus a unit case next to
navigation-bridge-traversal-location.test.ts.fix(router): follow a server redirect on Back/Forward to a fetched en…
fix(router): superseded push no longer follows a stale redirect; boun…