Skip to content

[DO NOT MERGE YET] Navigation API stand-in β€” stopgap pending remix-run/remix#11641 - #924

Closed
kentcdodds wants to merge 1 commit into
mainfrom
cursor/sentry-k2-kody-remix-client-b35b
Closed

kentcdodds wants to merge 1 commit into
mainfrom
cursor/sentry-k2-kody-remix-client-b35b

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 24, 2026 •

Copy link
Copy Markdown
Member

⚠️ Not for merge β€” awaiting Kent's decision

This is a stopgap for an upstream Remix bug (remix-run/remix#11641), not a fix for anything in our code. A separate agent is building a minimal reproduction repository to attach to that issue so it can be fixed upstream instead.

Opened so the workaround is reviewable if you want it in the meantime. Do not merge while the upstream fix is still in play. Merging deploys to heykody.dev.

Split out of #921. The other half of that PR β€” our own update-during-render bug, KODY-CLOUDFLARE-10 β€” was merged separately.

The bug this works around

@remix-run/ui's startNavigationListener reads window.navigation.updateCurrentEntry() with no feature check, and run() calls it during boot:

export function startNavigationListenerImpl(signal, options) {
  let navigation = window.navigation
  navigation.updateCurrentEntry({ ... })   // throws when the API is absent

Why this is worth your attention despite a 4-event count

Affected users report nothing at all. The throw happens inside run(), so run() never returns β€” which means entry.tsx never reaches the two lines after it:

const app = run({ ... })                     // ← throws here

app.addEventListener('error', (event) => {   // ← never registered
	captureClientException(event.error)
})
void app.ready()                             // ← never called

So for exactly the browsers hitting this, our client-side error reporting is never wired up. Every client error those users encounter β€” this one and any other β€” goes unreported. The event count is not a measure of how many users are affected; it is a measure of how thoroughly the bug hides itself.

Who is affected

The Navigation API is far newer than it looks. It reached Firefox in 147 and WebKit in 26.2, both in early 2026:

Engine First version with the Navigation API Still affected
Chrome / Edge 102 (2022) no
Firefox 147 (Jan 2026) 146 and below
Safari / WebKit 26.2 iOS 26.1 and below, all browsers

Every browser on iOS is WebKit, so on iOS ≀ 26.1 this hits Safari, Chrome for iOS, and in-app browsers alike. The Sentry group matches exactly: Firefox 141 on Ubuntu, Chrome Mobile iOS 150 on iOS 26.1, and the X in-app browser on iOS 26.1.

What actually breaks (corrected)

I initially assumed hydration died completely. Testing against a real Firefox 146 disproved that, and I would rather you have the accurate version: the throw lands after createFrame(...), so hydration already in flight survives and the app stays broadly usable β€” client-side routing still intercepts link clicks on production today.

The concrete costs are:

  1. An unhandled TypeError on every page load for affected visitors.
  2. No client-side error reporting for those visitors, per above.
  3. The Remix runtime's navigate interception never installs (we do not rely on it β€” see below).

I posted the same correction on the upstream issue.

What the stopgap does

Kody never uses the Navigation API. client-router.tsx drives SPA navigation with history.pushState and a document-level click handler, and it calls preventDefault() before the Remix listener could ever intercept. So a stand-in that never emits navigate is behaviourally identical to the real API for this app, and it is a strict no-op wherever the real API exists (if (win.navigation) return false).

Only the members the Remix runtime reads are implemented β€” updateCurrentEntry, entries, navigate, plus EventTarget. It is one self-contained file plus two lines in entry.tsx, removable in a single revert once upstream lands.

The case against merging it

  • It is a global shim over a framework internal, which is the category you flagged as wanting your eyes.
  • If upstream fixes this quickly, we carry a workaround for nothing.
  • Defining window.navigation where the browser deliberately does not is the kind of thing that can confuse other feature-detecting code, even though nothing in our bundle does that today (the Sentry SDK uses history patching, not the Navigation API).

The case for merging it as an interim

  • Affected users currently get zero client error reporting, so we are blind to their errors until upstream ships and we upgrade.
  • Firefox 147 and iOS 26.2 are both very recent, so the affected population is not a long tail yet β€” it is everyone who has not updated.

Verification

Playwright ships Firefox 146.0.1, the last release without the Navigation API, so this is the genuine failure condition in a real Gecko engine rather than a simulation. Production (no fix) versus a preview with the fix, same page, same minute:

Real Firefox 146 comparing production and a deployment with the stand-in

firefox-146-before-after-verification.mp4

Also covered by:

  • packages/worker/client/navigation-api-fallback.node.test.ts β€” the fallback contract, and that a real Navigation API is never replaced.
  • e2e/ssr-hydration.spec.ts β€” a Chromium test that shadows window.navigation and asserts the app still hydrates and handles SPA navigation, with a window marker proving the client router took the click rather than the browser doing a document load.

npm run validate passes.

System recap β€” extends existing primitives (medium risk β€” stopgap, not for merge)

Mode: recap Β· Base: main @ 569128cb Β· Head: 2b6b4970

Classification: extends β€” no new primitives; app-ui's boot sequence gains a
feature-detected global stand-in installed before the Remix runtime starts.

Primitives touched

Primitive Group Impact
app-ui surfaces extends β€” client entry defines window.navigation when the browser lacks it

System map

Client boot feature-detects the Navigation API and installs a stand-in before starting the Remix runtime, so run() completes and entry.tsx gets to wire up its error listener.

Legend: green = composes (wiring only) Β· amber = extended by this PR Β· red = new primitive Β· gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	appUi["app-ui<br/>Browser app (Remix 3)"]:::extended
	remixRuntime["remix-ui<br/>Remix client runtime"]:::untouched
	sentryTunnel["sentry-tunnel<br/>Same-origin error reporting"]:::untouched
	appUi -->|"installNavigationApiFallback(window) before run()"| remixRuntime
	remixRuntime -->|"run() returns, so app.addEventListener('error') is registered"| appUi
	appUi -->|"client errors reach Sentry on affected browsers again"| sentryTunnel
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Browser Before After
Has the Navigation API (Chrome, Edge, Firefox 147+, iOS 26.2+) unaffected unaffected β€” installNavigationApiFallback returns early
Lacks it (Firefox ≀146, iOS ≀26.1) unhandled TypeError per page load; entry.tsx never wires up error reporting no error; run() returns and error reporting is wired up

Invariants

No change to per-user isolation. The stand-in holds only the current entry's navigation state and no user data.

Open in WebΒ Open in CursorΒ 

Summary by CodeRabbit

  • New Features

    • Added support for app navigation in browsers that do not provide the Navigation API.
    • Navigation now preserves expected page state and supports standard link routing, including history replacement and back/forward behavior.
  • Bug Fixes

    • Improved client-side hydration and navigation reliability in environments without native Navigation API support.
    • Prevented navigation-related page errors when opening community listings and their detail pages.

Remix 3 beta's client runtime reads window.navigation.updateCurrentEntry()
unguarded while it boots (@remix-run/ui's startNavigationListener), so
run() throws on every browser without the Navigation API. That API only
reached Firefox in 147 and WebKit in 26.2, both in early 2026, so it
still covers Firefox 146 and below plus every browser on iOS 26.1 and
below.

The throw lands after createFrame() and before run() returns, so
hydration already in flight survives and the app stays broadly usable.
The real cost is an unhandled TypeError on every page load plus the
app.addEventListener('error', ...) wiring in entry.tsx that never runs,
so affected users report no client errors at all (KODY-CLOUDFLARE-17).

Kody never uses the Navigation API - client-router.tsx drives SPA
navigation through history.pushState and a document click handler - so a
stand-in that never emits 'navigate' behaves like the real thing here.
Only the members the Remix runtime reads are implemented.

This is a stopgap for remix-run/remix#11641 and should not merge while
the upstream fix is still in play.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b258b24-0c00-487b-8926-dd47e5263da4

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 569128c and 2b6b497.

πŸ“’ Files selected for processing (4)
  • e2e/ssr-hydration.spec.ts
  • packages/worker/client/entry.tsx
  • packages/worker/client/navigation-api-fallback.node.test.ts
  • packages/worker/client/navigation-api-fallback.ts

πŸ“ Walkthrough

Walkthrough

Adds a minimal Navigation API fallback, installs it during worker client startup, and adds unit and Playwright coverage for hydration and client-side navigation when window.navigation is unavailable.

Changes

Navigation API compatibility

Layer / File(s) Summary
Fallback implementation
packages/worker/client/navigation-api-fallback.ts
Adds navigation entry state, URL routing through location.assign or location.replace, unresolved navigation promises, and conditional fallback installation.
Client startup integration
packages/worker/client/entry.tsx, packages/worker/client/navigation-api-fallback.node.test.ts
Installs the fallback before run() and tests installation, state handling, navigation routing, and preservation of an existing API.
Browser hydration verification
e2e/ssr-hydration.spec.ts
Tests hydration and client-side listing navigation in a browser without the Navigation API.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant WorkerClient
  participant NavigationFallback
  participant ClientRouter
  Browser->>WorkerClient: Start application without window.navigation
  WorkerClient->>NavigationFallback: Install fallback
  WorkerClient->>ClientRouter: Start runtime
  ClientRouter->>NavigationFallback: Read navigation state and navigate
  NavigationFallback->>Browser: Assign or replace destination
Loading

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly describes the temporary Navigation API stand-in and its stopgap purpose.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/sentry-k2-kody-remix-client-b35b

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 24, 2026 21:35
@github-actions

Copy link
Copy Markdown
Contributor

πŸ”Ž Preview deployed: https://kody-pr-924.kody-a99.workers.dev

Worker: kody-pr-924
D1: kody-pr-924-db
KV: kody-pr-924-oauth-kv

Mocks:

@kentcdodds kentcdodds closed this Jul 25, 2026
@cursor
cursor Bot deleted the cursor/sentry-k2-kody-remix-client-b35b branch July 30, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants