fix(api): mark schema-validation errors as non-retryable

The proxy returned `{fonts: null, total: 0}` for empty results, which
fetchProxyFonts surfaced as a generic Error. fontCatalogStore wrapped it
as FontNetworkError, and TanStack retried 3× with exponential backoff —
pinning the loading skeleton for ~7s before settling on an empty list.

Schema mismatches are deterministic; retrying only delays surfacing the
contract violation.

- shared/api/queryClient: introduce NonRetryableError marker class.
  The default retry handler short-circuits when it sees this so any
  store using the shared client gets fail-fast behavior for free.
- entities/Font/lib/errors: FontResponseError extends NonRetryableError.
- entities/Font/api/proxy/proxyFonts: throw FontResponseError (was a
  bare Error). Document that ProxyFontsResponse.fonts is always an array.
- entities/Font/.../fontCatalogStore.fetchPage: preserve a
  FontResponseError raised lower in the stack instead of re-wrapping
  it as FontNetworkError.
- features/FilterAndSortFonts/api/filters: throw NonRetryableError on
  invalid filters payloads and document the array-never-null contract.
This commit is contained in:
Ilia Mashkov
2026-05-28 21:37:23 +03:00
parent c90a258f6c
commit 7f20f36d0a
6 changed files with 59 additions and 14 deletions
+16 -1
View File
@@ -1,5 +1,15 @@
import { QueryClient } from '@tanstack/query-core';
/**
* Marker base class for errors that retrying will never fix — schema-validation
* failures, unauthorized responses, contract violations, etc.
*
* The queryClient retry handler short-circuits when it sees this; without it,
* a non-transient backend bug pins the UI through the full retry budget
* (default 3× exponential backoff ≈ 7s).
*/
export class NonRetryableError extends Error {}
/**
* Data remains fresh for this long after fetch. Stores that override
* staleness (e.g. filtered queries) can use 0 to bypass.
@@ -51,7 +61,12 @@ export const queryClient = new QueryClient({
* Refetch on mount if data is stale
*/
refetchOnMount: true,
retry: QUERY_RETRY_COUNT,
retry: (failureCount, error) => {
if (error instanceof NonRetryableError) {
return false;
}
return failureCount < QUERY_RETRY_COUNT;
},
/**
* Exponential backoff: 1s, 2s, 4s, 8s... capped at 30s
*/