fix(frontend): harden exchange-token routing against transient control-plane failures#4472
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…l-plane failures Transient failures of cross-region control-plane calls could silently flip the app into "control plane disabled" mode or send tenant requests unauthenticated to the wrong host, surfacing as intermittent hard 404s. Serve the last known metadata answer instead of caching failures, reject requests with a typed error when the token fetch fails, bound control-plane calls with a 15s timeout, renew tokens in the background, and fix a staleTime calculation that halved the effective token lifetime.
igor-kupczynski
force-pushed
the
exchange-token-resilience
branch
from
July 21, 2026 15:46
d363d41 to
0ce663b
Compare
…hable The metadata query now treats first-load 5xx as transient and retries, so a dead control-plane upstream in dev/e2e (which the Vite proxy surfaced as a 500) broke control-plane detection and failed every Cypress spec. Production OSS returns 404 for unregistered control-plane routes; make the dev proxy do the same when the upstream is down.
igor-kupczynski
marked this pull request as ready for review
July 21, 2026 16:13
…first load The first-load metadata check accepted any 4xx as the definitive "no control plane" answer and cached it for the query's staleTime. That bucket included 408/429, which this PR's own retry policy classifies as transient, so a rate-limited first check could disable exchange-token routing for five minutes and misroute tenant requests. Gate the definitive branch on the shared retryable-error classification so 408/429 fall through to the throw path and get retried.
abelanger5
approved these changes
Jul 22, 2026
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.
Description
When the app runs with a control plane (Hatchet Cloud), every tenant-scoped API request is transparently authenticated by an axios interceptor: it fetches a short-lived exchange token from the control plane and rewrites the request's
baseURLto the tenant shard's API. For users whose shard is in a different region than the control plane, those token/metadata calls are cross-region.I've observed two failure paths which are worth fixing for a better UX:
null, which React Query cached as a successful "no control plane" answer. For the next 60s the interceptor skipped routing/auth entirely, so requests meant for the tenant shard went to the frontend's own origin — where those routes don't exist — producing 404s that are never retried (4xx).console.erroronly) and the request proceeded unauthenticated to the wrong host: same misleading 404.On top of that, token renewal happened synchronously on the request path (blocking an interactive request on a cross-region round trip every few minutes), there was no client timeout below the browser's ~60s default, and a
staleTimecalculation bug made tokens go stale at half their intended lifetime, doubling renewal traffic.Before / after
sequenceDiagram participant UI as Browser UI participant INT as axios interceptor participant RQ as React Query cache participant CP as Control plane participant SH as Tenant shard UI->>INT: tenant API request INT->>RQ: control plane enabled? exchange token? RQ->>CP: metadata / token fetch (cross-region) CP--xRQ: transient failure rect rgb(255, 235, 235) Note over UI,SH: BEFORE — silent fallbacks RQ-->>INT: error swallowed, "control plane disabled" cached 60s INT->>CP: request proceeds unrouted + unauthenticated (wrong host) CP-->>UI: 404 — a 4xx is never retried, page hard-fails until the cache expires end rect rgb(235, 245, 235) Note over UI,SH: AFTER — fail loudly, recover fast RQ-->>INT: last known good answer (or retry transient errors on first load) alt token fresh (renewed by 30s background timer) INT->>SH: routed to shard with Bearer token SH-->>UI: 200 else control plane genuinely unreachable RQ-->>INT: bounded retries exhausted (15s timeout per attempt) INT--xUI: request rejected with typed error — no misroute, no retry storm end endType of change
What's Changed
More robust handling of control plane blips
Improve retries
api.ts)Keep token renewal off the hot path
query-client.tsx)staleTimemath error expired tokens at half their lifetime → 2× the renewal traffic (exchange-token.ts)Checklist
Changes have been:
🤖 AI Disclosure
I acknowledge that an LLM was used in the creation of this Pull Request, in accordance with Hatchet's AI_POLICY.md.
Details: Cursor agent (Claude) was used to investigate the incident, implement the fix, and run adversarial correctness and code-quality reviews. All changes were verified with typecheck, unit tests, and lint; final review by a human.