Skip to content

Show a failed page load only on its error page, without a duplicate toast or RUM report - #1878

Merged
dawsontoth merged 7 commits into
claude/1679-signout-auth-racesfrom
claude/1808-route-load-single-report
Oct 12, 2026
Merged

dawsontoth merged 7 commits into
claude/1679-signout-auth-racesfrom
claude/1808-route-load-single-report

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on the auth stack: #1813 → #1815 → #1816 → #1854 → this PR. The base is claude/1329-scope-auth-checks-to-org (#1854), so the diff here is only the #1808 work. Merge the stack in order, then retarget this PR to stage. It lands alongside #1809 (stacked on #1804), which it doesn't share a line with — see Landing with #1809.

⊙ Problem

When a page's data fails to load (the org, cluster, or instance registration-info read in a route's beforeLoad or loader), Studio shows the route's error page and also a toast with the same failure in raw server words. React Query's global handler console.errors it as well, which RUM files as a console-sourced error next to the error boundary's own report (#1808). Driving the real router on this base with one org 403 and two reloads gave 3 GETs, 3 toasts and 3 console-sourced reports, on top of the boundary's.

❓ Your call: Is the requirement right as scoped? I think so: the error page already says what happened, and two Error Tracking issues with different fingerprints for one failure hide whether either is getting worse. This PR removes the query-cache half. #1809 removes the boundary half (re-catches, and the root handler's own console.error). It's one report only once both are in, so this body's Closes #1808 should only fire on whichever merges second. Merge this after #1809, or drop that line.

Closes #1808

💡 Solution

  • A route read claims its query while the route awaits it. ensureRouteQueryData now takes the route function's own arguments. When the read has to fetch and the call isn't a preload, it claims the query until the read settles or the router aborts the load (claimForRoute).
  • queryErrorHandler skips a claimed failure (here): no toast, no console.error. The error page is the one message and the root onCaughtError the one report.
  • Everything else is unchanged: a background refetch on a page that rendered, imperative fetches, reads the cache answers, a load the user navigated away from (the router aborts it, which releases the claim), and every preload still toast and report as today.

⚠️ Look hardest: the claim has to be live when React Query reports the failure. QueryCache.onError runs synchronously in the failing Query.fetch's catch, before the rejection reaches the route (query-core 5.104.1 query.ts:799), and the claim is released in finally, after it. A test checks the claim is held at onError time.

Landing with #1809

The two touch no common lines. I merged #1809's head (9581286) into this branch locally, cleanly, and re-ran the route tests with #1809's real reactRootErrorHandlers.onCaughtError and exact counts. One org 403 plus two reloads gave addReactError ×1, console.error ×0, toast ×0, and one "No Access to This Organization" page. Hover-then-click on the org, and a loader read the navigation runs, gave the same. With this PR's handler check removed, the merged tree logs one console report per GET (4), so #1809 alone doesn't get there. tsc -b is clean on the merged tree. Either can merge first.

⚖️ Alternatives

❓ Your call: A navigation adopts a loader its hover preload still has in flight instead of running it again (router-core 1.171.34, verified with the real router). So one path still reports twice: hover an instance link, click before registration info answers, and it fails (toast and console, then the boundary). A test pins it. I don't claim loader preloads. That would close this path but silence every failed hover nobody clicked: the router doesn't log a preload's route failure (load-client.ts:2161), and the planning reviewer and both implementation reviewers flagged that silent case. Closing it with nothing silent means moving registration info from loader to beforeLoad, which a navigation does re-run. Three components read it through useLoaderData, so that's its own change. Either way is easy to revisit.

  • The issue's option 1, a query meta flag set by route loads: rejected. meta lives on the shared Query and the last writer wins. When a navigation joins a fetch already in flight (the hover-then-click path), Query.fetch returns the in-flight promise before applying the caller's options (query.ts:594-615), so the flag never lands where it's needed most. The claim is the same explicit, route-only marker, scoped to the load instead of to the query.
  • The issue's option 2, skip a failure with no data and no observers: rejected. It also silences fetchQuery/ensureQueryData failures nothing renders (SearchDialog, EditorViewProvider, secrets.ts), dropping their only report.
  • Opt the route queries out in their option factories: rejected. The same factories back the polling useQuerys (the org query polls every 10 s), so a background failure would go quiet.
  • Dedupe downstream, in beforeSend or the root handler: rejected. The toast stays, and the console event is already sent.
  • The planning review (codex) returned better-alternative-exists: don't claim beforeLoad preloads, because a navigation runs beforeLoad again. I adopted it. After implementation round 1, I went further and dropped preload claims entirely.

🔧 Changes

✅ Verification

🤖 Generated with Claude Code

🤖 Generated by Claude Code (Claude Opus 5.5); posted via @dawsontoth.

Related PRs: #1809 overlaps (lands with it: this PR drops the query-cache report, #1809 the boundary's duplicates; no shared lines), #1816 overlaps (stacked on it; changes its helper's signature), 1 others independent
Complexity: complicated

Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth),cursor-muse(no-receipt); declined=cursor-grok,cursor-kimi; rounds=2; full=1 @ 474b549

Review-Attention: study ~9m (raised: degraded review, open blocker) @ 474b549

dawsontoth and others added 3 commits October 10, 2026 23:20
…d of failing it

Clearing the query cache at sign-out destroys every query, and a fetch still in
flight rejects its awaiters with React Query's `CancelledError`. Route
`beforeLoad`s and loaders await `ensureQueryData`, so a route still loading when
the cache was cleared failed with that cancellation: the router rendered it as
the route's error (its `CatchBoundary`), `main.tsx`'s `onCaughtError` reported
it to RUM, and only then did the signed-out state reach the router's guards and
redirect to sign-in. RUM: 8 events / 6 sessions in 30 days, every one from
`clearAuthStateLocally` on a CM 401 — e.g. a deep link opened with an expired
session, where `GET /User/current` 401s while the org and cluster routes are
still fetching.

`ensureRouteQueryData` wraps `ensureQueryData` for the three route-data
producers (the organization and cluster layouts' `beforeLoad`, and the
registration-info loader behind the instance and config routes). It reads the
cloud slot's sign-out generation before the fetch; a `CancelledError` that
arrives after that generation moved never settles, and the router — which races
every `beforeLoad` and loader against its abort signal — drops the load when the
guards' redirect supersedes it. Any other error, or a cancellation no sign-out
caused, is rethrown as before.

Closes #1794

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion is shown

Every page load checked every instance and cluster the browser had ever signed into, across all
organizations: the root auth subscription looped over Studio:PotentiallyAuthenticated and sent a
credentialed user_info to each. Viewing one organization and refreshing therefore contacted
self-hosted instances in another (#1329).

The root subscription now checks only the app sign-in. An entity is checked when something
subscribes to it (useInstanceAuth on a cluster card, cluster home, instance cell or the instance
nav), or when the instance-layout guard navigates into it: the guard starts that check itself
through the new startSignInCheck, because the route's loader runs before any subscriber mounts.

useInstanceAuth now subscribes to the same entity its initial state reads, so a caller that names
no entity still checks the route's, and it returns the new entity's state as soon as its entity
changes instead of the previous one's.

Closes #1329

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oast or a second RUM report

A route that reads data in `beforeLoad` or `loader` reported one failure twice: React Query's
global handler `console.error`ed it (a console-sourced RUM error) and toasted it, and the route's
error boundary rendered it and reported it again through the root `onCaughtError`.

`ensureRouteQueryData` now takes the route function's own arguments and, when the read has to
fetch, claims the query until the read settles or the router abandons the load.
`queryErrorHandler` skips a claimed failure, so the error page is the one message and the boundary
the one report. A background refetch, an imperative fetch, a read the cache answers, a load the
user navigated away from, and every preload are unclaimed and still toast and report as before.

A navigation runs `beforeLoad` again and claims a read its hover preload left in flight, so that
path reports once too. It adopts an in-flight loader preload without running the loader, so that
one failure still reports twice; claiming loader preloads would close it only by silencing every
failed hover nobody clicked.

Closes #1808

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a query-claiming mechanism during route loading to ensure that route load failures are handled and reported solely by the route's error boundary, suppressing global error toasts and RUM reports for active route loads. It refactors ensureRouteQueryData to accept a RouteLoad context, implements query tracking in routeQueryClaims.ts, and updates the global query error handler to respect these claims. Comprehensive unit and integration tests, along with design documentation, have been added to verify the new behavior. I have no additional feedback to provide as there are no review comments to address.

dawsontoth added a commit that referenced this pull request Oct 11, 2026
… the held filter

#1878 (stacked on the same base) makes a failure a route load is awaiting
report only through that route's error page: no toast, no console.error.
That is the page-load 403 this branch's filter held, so the filter would only
duplicate it and conflict on the same lines of queryErrorHandler. With the
Dashboard gate keeping the error page from rendering until a user is
confirmed, the two together leave an expired session's deep link with no
error page, toast, or report, and a live non-member with the error page.

Removes installSignedOutFailureFilter and its queryClient hook; the mounted
test now pins what the Dashboard gate guarantees.

Refs #1546

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth added this pull request to stack #1911 October 11, 2026 22:27
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 68.18% 10261 / 15049
🔵 Statements 68.31% 10942 / 16018
🔵 Functions 61.55% 2647 / 4300
🔵 Branches 62.97% 7773 / 12343
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/cluster/clusterLayoutRoute.ts 100% 100% 100% 100%
src/features/instance/registrationInfoLoader.ts 100% 100% 100% 100%
src/features/organization/routes.ts 76.19% 50% 63.63% 76.19% 24, 35, 41, 48, 54
src/react-query/queryClient.ts 97.95% 97.67% 83.33% 97.95% 83
src/react-query/routeQueryClaims.ts 100% 90% 100% 100%
src/router/ensureRouteQueryData.ts 100% 100% 100% 100%
Generated in workflow #2155 for commit 33b0f43 by the Vitest Coverage Report Action

@dawsontoth
dawsontoth marked this pull request as ready for review October 12, 2026 15:58
@dawsontoth
dawsontoth requested a review from a team as a code owner October 12, 2026 15:58
@dawsontoth
dawsontoth removed this pull request from stack #1911 October 12, 2026 16:00
Base automatically changed from claude/1329-scope-auth-checks-to-org to claude/1794-signout-route-cancel October 12, 2026 16:01
Base automatically changed from claude/1794-signout-route-cancel to claude/1679-signout-auth-races October 12, 2026 16:01
…into claude/1808-route-load-single-report

The base now holds rebased copies of #1816 and #1854, whose tree is identical to 71805a6, which this branch already contains; keeping this branch's tree resolves the duplicate-history conflicts without changing any file.
@dawsontoth
dawsontoth merged commit 34eb689 into claude/1679-signout-auth-races Oct 12, 2026
1 check passed
@dawsontoth
dawsontoth deleted the claude/1808-route-load-single-report branch October 12, 2026 16:04
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.

A failed route load is reported to RUM twice and shown twice — React Query's console.error and toast duplicate the route error page

1 participant