Skip to content

fix(frontend): transient 429 handling and guided no-snapshot states - #226

Merged
parthrohit22 merged 4 commits into
Second-Origin:devfrom
parthrohit22:fix/169-178-honest-recoverable-states
Jul 27, 2026
Merged

fix(frontend): transient 429 handling and guided no-snapshot states#226
parthrohit22 merged 4 commits into
Second-Origin:devfrom
parthrohit22:fix/169-178-honest-recoverable-states

Conversation

@parthrohit22

Copy link
Copy Markdown
Collaborator

Summary

Two related honest/recoverable-state fixes, bundled because both extend the
exact same pattern and touch the same frontend error/state-handling layer:

Linked issue

Closes #169
Closes #178

Roadmap alignment

  • Roadmap §23 workstream this advances: W0 / Truth contract — every
    product surface must present an honest, evidence-grounded state; a
    transient condition must never be indistinguishable from a real failure,
    and a recoverable state must never be indistinguishable from a dead end.
  • §28 market-fit criterion this moves toward: Trusted output / Repeat
    use
    — a user who hits a transient rate limit or an unsealed snapshot must
    be able to recover and keep using the product, not bounce off a
    misleading terminal error.
  • Accepted evidence it is real: regression tests below reproduce both original
    bugs (429 read as "Analysis Failed"; Review/Insights/Documentation showing
    a generic unguided error) and pass only after the fix; verified locally.

What changed

#169 (backend contract already existed — this is a frontend fix)

  • shared/services/api/errors.ts: ApiError now carries the raw
    Retry-After header; new parseRetryAfter() handles delta-seconds,
    HTTP-date, and defends against missing/malformed values. A new
    ApiError.retryAfterSeconds getter prefers the header, falling back to the
    backend's details.retryAfterSeconds body field.
  • shared/services/api/client.ts: 429 is excluded from the blind
    transport-level retry (previously retried with fixed 1s/2s backoff
    regardless of Retry-After — itself the retry-storm risk). All three
    ApiError construction sites now capture the header.
  • features/analysis/hooks/useAnalysisPipeline.ts: a new 'rate-limited'
    connectionStatus, mirroring the existing network-error recovery pattern.
    A 429 during polling starts a Retry-After-driven countdown and
    auto-retries once it elapses, bounded to MAX_RATE_LIMIT_AUTO_RETRIES = 3
    attempts and RATE_LIMIT_MAX_WAIT_SECONDS = 120 per wait. The manual
    restart() action also respects the cooldown instead of accepting another
    click mid-wait.
  • app/pages/AnalysisPipelinePage.tsx: an accessible (role="status")
    countdown banner replaces the terminal "Analysis Failed" state for a 429;
    the "Restart analysis" button is disabled and labelled with the remaining
    wait during a cooldown.

#178

  • features/review/hooks/useReview.ts, features/insights/hooks/useInsights.ts,
    features/documentation/hooks/useDocumentation.ts: each gained a
    noSnapshot flag, set exactly like the existing useDependencies.ts
    pattern (isApiError(caught) && caught.isNotFound).
  • app/pages/EngineeringReviewPage.tsx, InsightsPage.tsx,
    DocumentationPage.tsx: each renders the same EmptyState — "No sealed
    snapshot yet" with a "Run analysis" action — that DependenciesPage.tsx
    already established, instead of a generic destructive-styled error.

Scope note on #178's premise: I checked productSurfaces.tsx and none of
the six named surfaces (Architecture, Dependency Graph, AI Workspace,
Engineering Review, Documentation, Insights) are actually dead or
unreachable — all are ready/preview in the registry. The real,
reproducible gap was narrower (see above), and I scoped the fix to that
rather than reworking navigation. Architecture gets the equivalent fix under
#217 (separate PR, since it's a backend contract change).

Acceptance criteria completed

#169

  • Parse and respect Retry-After.
  • Model 429 as transient rather than terminal failure.
  • Prevent manual and automatic retry storms (transport-level blind retry
    removed; manual restart disabled during cooldown).
  • Bound automatic retries (MAX_RATE_LIMIT_AUTO_RETRIES = 3,
    RATE_LIMIT_MAX_WAIT_SECONDS = 120).
  • Accessible countdown/status messaging (role="status").
  • Tested missing, malformed, delta-seconds, and date-based Retry-After
    values (errors.test.ts).
  • Tested cleanup on unmount and on job success/failure
    (useAnalysisPipeline.test.ts).

#178

Testing performed

npm --prefix apps/frontend run test -- --run
  → Test Files  35 passed (35) / Tests  198 passed (198)

npm run lint:frontend
  → clean, no errors

npm run build:frontend
  → tsc -b && vite build succeeded, no type errors

git diff --check
  → clean, no whitespace errors

Backend was not touched by this PR; the backend gate (pytest) was not
re-run here since no backend file changed (verified via git diff --stat).

Screenshots

Not applicable — the rate-limited countdown and no-snapshot states reuse
existing visual patterns already shipped (the warning-styled connection banner
for #169, the EmptyState component already visible on the Dependencies page
for #178). No new visual design.

Security and data considerations

None. No auth/owner-scoping change. The 404 detection reuses the existing
ApiError.isNotFound check already used by useDependencies.ts. No new data
is stored, logged, or sent to the backend. The Retry-After header is parsed
defensively (regex-guarded, Date.parse result validated) so a malformed or
adversarial header value cannot produce NaN/negative delays or an infinite
wait — it falls back to a fixed default instead.

Dependencies and blocked work

None. Independent of PR #225 (#217) — both extend the same established
noSnapshot/honest-state pattern but touch different files (#217 is
backend-contract + Architecture-only; this PR is Review/Insights/Documentation

  • the analysis poller).

Scope changes or remaining work

None for #169/#178 as scoped. AI Workspace was deliberately left out of the
#178 fix: it doesn't have the same blank/dead-end problem the issue
describes — its chat interface stays interactive and only shows an error
after an explicit user action (asking a question), which is a materially
different, lower-severity situation not covered by the issue's evidence.

Contributor checklist

  • This PR targets dev
  • I claimed both issues and had them assigned or acknowledged before starting substantial work
  • The branch was created from an up-to-date upstream/dev
  • The branch is rebased on the latest upstream/dev
  • This PR addresses two issues sharing the same state model and files (justified bundling, not scope creep)
  • This PR advances a §23 workstream toward a §28 market-fit criterion (Roadmap alignment filled)
  • Every acceptance criterion I claim as complete is actually complete
  • Relevant tests pass
  • Documentation is updated for any user-visible change (none required — no public doc described the old fallback behaviour as current)
  • No secrets, credentials, local env files, or generated artifacts are included
  • No unrelated files were changed
  • Closing syntax (Closes) is used only because both issues are fully resolved
  • Dependencies and follow-up work are linked

The API client blindly retried a 429 with fixed exponential backoff,
ignoring Retry-After entirely -- itself a retry-storm risk. 429 is now
excluded from that transport-level retry; ApiError parses Retry-After
(delta-seconds or HTTP-date, defensive against missing/malformed values)
and exposes it as retryAfterSeconds.

The analysis poller treats a 429 as transient (mirroring the existing
network-error recovery pattern) instead of the terminal "Analysis Failed"
state: it shows an accessible countdown, auto-retries once the window
elapses, and bounds automatic retries. The manual "Try again"/"Restart
analysis" actions are disabled for the same cooldown to prevent
retry-storming from repeated manual clicks.
…tation

A repository can be "completed" (analysed) yet still have no sealed ri.v1
snapshot for its current revision, the same 404 Dependencies/Architecture
already distinguish. Engineering Review, Insights, and Documentation
previously collapsed that into a generic error message with a "Retry"
button that could never succeed. They now show the same "No sealed
snapshot yet -- Run analysis" guided empty state Dependencies already
established, reusing the existing EmptyState component.

None of the named product surfaces (Architecture, Dependency Graph, AI
Workspace, Engineering Review, Documentation, Insights) are actually
dead or unreachable in the current nav registry -- this narrows the
issue to the concrete, provable gap found on inspection.
…ncelled

The standalone rate-limited banner and the cancelled banner's disabled,
countdown-labelled Restart button communicated the same cooldown
independently, so a 429 hit while restarting a cancelled job showed both
at once. Suppress the standalone banner while cancelled.
@parthrohit22
parthrohit22 marked this pull request as ready for review July 27, 2026 18:48
@parthrohit22
parthrohit22 merged commit 053ea09 into Second-Origin:dev Jul 27, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant