Skip to content

Fix retry predicate's dead instanceof Error check for 404s - #68

Merged
abayomicornelius merged 4 commits into
StellarSend:mainfrom
richardtoms100:fix/retry-predicate-error-shape-60
Aug 18, 2026
Merged

Fix retry predicate's dead instanceof Error check for 404s#68
abayomicornelius merged 4 commits into
StellarSend:mainfrom
richardtoms100:fix/retry-predicate-error-shape-60

Conversation

@richardtoms100

Copy link
Copy Markdown
Contributor

Summary

The global React Query retry predicate checked error instanceof Error to skip retrying 404s, but the axios response interceptor always rejected with a plain { code, message, details } object literal, never a real Error. The check could never match, so the // Don't retry 404s branch was dead in practice — every backend-routed query failure retried up to twice, including genuine 404s the code explicitly intended to fail fast on.

Implemented the issue's second suggested fix (making the interceptor reject with a real Error subclass), combined with the first's refinement (checking a stable code instead of message substring-matching):

  • Added ApiRequestError extends Error implements ApiError (src/lib/api.ts) carrying code/details as real properties. The interceptor now rejects with an instance of it instead of a plain object — extracted into a standalone normalizeApiError function so it's unit-testable without relying on axios's internal interceptor-handler storage.
  • code prefers the backend's own data.code; when the backend doesn't send one, a 404 status is normalized to a stable 'NOT_FOUND' code (derived from the real HTTP status — a reliable, backend-convention-independent signal) instead of falling through to 'UNKNOWN_ERROR'.
  • This also fixes instanceof Error everywhere else in the app that assumed it, not just this one predicate — every hook already types its query error as useQuery<T, Error>, which was previously a lie about the actual runtime shape.
  • Extracted the retry predicate into src/lib/queryRetry.ts as shouldRetryQuery, checking error instanceof ApiRequestError && (error.code === 'NOT_FOUND' || error.message.includes('not found')) — code-based as the primary signal, message-substring kept as a fallback. Pulled out of App.tsx both for direct testability (per the issue's own suggested testing strategy) and because react-refresh/only-export-components (enforced here at --max-warnings 0) requires component files to only export components.

Test plan

  • New tests for normalizeApiError/ApiRequestError: produces a real Error instance, carries backend-supplied code/message/details through unchanged, derives 'NOT_FOUND' from a 404 status when the backend sends none, prefers a backend-supplied code when present, falls back to 'UNKNOWN_ERROR'/a generic message otherwise
  • New tests for shouldRetryQuery: doesn't retry an ApiRequestError with code: 'NOT_FOUND', doesn't retry one whose message mentions "not found" under a different code, retries other ApiRequestErrors and non-ApiRequestError values up to the failure-count limit (proving a plain object that happens to look 404-shaped is correctly not special-cased)
  • npm test — 106/106 passing
  • npx tsc --noEmit — clean
  • npm run lint — clean (--max-warnings 0)
  • npm run build — succeeds

Closes #60

@abayomicornelius

Copy link
Copy Markdown
Contributor

clean work

@abayomicornelius
abayomicornelius merged commit efffa7f into StellarSend:main Aug 18, 2026
4 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

Development

Successfully merging this pull request may close these issues.

QueryClient's 'don't retry 404s' predicate checks error instanceof Error, but the axios interceptor never rejects with a real Error

2 participants