Repository navigation
Show a failed page load only on its error page, without a duplicate toast or RUM report - #1878
Merged
dawsontoth merged 7 commits intoOct 12, 2026
Conversation
…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>
Contributor
There was a problem hiding this comment.
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
added this pull request to stack #1911
October 11, 2026 22:27
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…into claude/1794-signout-route-cancel
…' into claude/1329-scope-auth-checks-to-org
…-org' into claude/1808-route-load-single-report
dawsontoth
marked this pull request as ready for review
October 12, 2026 15:58
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
When a page's data fails to load (the org, cluster, or instance registration-info read in a route's
beforeLoadorloader), Studio shows the route's error page and also a toast with the same failure in raw server words. React Query's global handlerconsole.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.Closes #1808
💡 Solution
ensureRouteQueryDatanow 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).queryErrorHandlerskips a claimed failure (here): no toast, noconsole.error. The error page is the one message and the rootonCaughtErrorthe one report.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 realreactRootErrorHandlers.onCaughtErrorand exact counts. One org 403 plus two reloads gaveaddReactError×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 -bis clean on the merged tree. Either can merge first.⚖️ Alternatives
metaflag set by route loads: rejected.metalives on the sharedQueryand the last writer wins. When a navigation joins a fetch already in flight (the hover-then-click path),Query.fetchreturns 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.fetchQuery/ensureQueryDatafailures nothing renders (SearchDialog,EditorViewProvider,secrets.ts), dropping their only report.useQuerys (the org query polls every 10 s), so a background failure would go quiet.beforeSendor the root handler: rejected. The toast stays, and the console event is already sent.better-alternative-exists: don't claimbeforeLoadpreloads, because a navigation runsbeforeLoadagain. I adopted it. After implementation round 1, I went further and dropped preload claims entirely.🔧 Changes
src/react-query/routeQueryClaims.ts(new):claimForRoutekeeps a reference count perQueryobject in aWeakMap, so overlapping loads of one query (a load plus aninvalidate) don't release each other. Release is idempotent and runs fromfinallyor on abort. A load the router already aborted claims nothing.isClaimedByRouteis the handler's check.src/react-query/queryClient.ts:queryErrorHandlerreturns early on a claimed failure, after the restart-noise check. Its doc says why.src/router/ensureRouteQueryData.ts: takes aRouteLoad(context,abortController,preload), the subset of TanStack'sbeforeLoad/loaderarguments it needs. The call sites pass the real objects, so a rename upstream breaks the build. It builds the sameQueryensureQueryDatawill and claims only an uncached read. Its doc states the contract: a caller that catches the rejection takes on reporting it. The sign-out cancellation path from fix(router): let a sign-out's cache clear abandon a route load instead of failing it #1816 is unchanged.RouteLoadplus params.src/router/DESIGN.md(new), indexed from the rootDESIGN.md: the ownership rule, why preloads don't claim and the adopted-loader duplicate, and a limit. If two parallel loaders in one route chain ever read different queries and both fail, only the rendered failure is reported. Today they all read registration info.✅ Verification
routeLoadFailureReporting.test.tsxdrives the real router through the production chain (root → dashboard → orgs → org layout) with the app'squeryClient, mocking onlyapiClient.getandaddReactError:@error.source:consoleAxiosErrors per route-load failure should drop to zero.isClaimedByRoutecheck removed (the base's handler path), the first e2e test fails withexpected [ …4 ] to deeply equal [](one console report per GET), and 3 more e2e tests plus the unit test go red. Mutations: no abort release turns the navigate-away and abort tests red; claiming preloads turns the hover tests red; claiming cached reads turns the cached-read test red; dropping the reference count turns both overlap tests red.ensureRouteQueryData.test.ts: the claim is held during the await and atonErrortime, covers the sameQueryunder a customqueryKeyHashFn, is released on abort, and is never taken for a preload or a cached read. The existing sign-out tests run on the new signature.routeQueryClaims.test.tscovers identity, reference counting, abort then release, a late abort, and an already-aborted signal.queryClient.test.ts: a claimed failure gets no toast and noconsole.error, and gets both once released.vitest(405 files, 3,733 tests pass), oxlint, dprint and commitlint, all clean;tsc -b0. Not checked in a browser: what changes is the toast and console channel, which the jsdom tests observe directly.no-receipt), and the Harper domain leg failed auth (OAuth session expired), so outside findings weren't adjudicated and I triaged them myself. Taken: no preload claims (round 1 major from both — a loader preload's claim silenced hover-only failures and any background fetch it joined), no claim for a cached read (codex minor), and comment trims. Kept open: the adopted-loader duplicate (the❓above), and codex's point that the fixture's root handler leaves out this base'sconsole.error. That channel is Report each failed route load to RUM once, and offer Try Again when the server errors or can't be reached #1809's, and the merged-tree run uses the real handler with exact counts. Declined: codex's minor that building theQuerybeforeensureQueryDatarepeats work. It's once per route read on navigation, next to a network call.🤖 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