Skip to content

Back/Forward to an entry the server now redirects is not followed: "Unprocessable popstate response" and an error boundary #1047

Closed

Description

@ivogt

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.

Activity

  1. ivogt commented on Oct 4, 2026

    @ivogt
    CollaboratorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions