abhigyanpatwari/GitNexus · error · ResilientFetchExhaustedError
Request failed after retries (HTTP ${response.status})
Error message
Request failed after retries (HTTP ${response.status}) What it means
ResilientFetchExhaustedError is thrown by resilientFetch when the retry budget for a retryable-status outcome (HTTP 5xx or 429) is exhausted on the final attempt. It carries the last Response object so callers can inspect headers/status. A recordFailure() is recorded against the breaker before throwing, so repeated exhaustion contributes to tripping the circuit.
Source
Thrown at gitnexus-shared/src/integrations/resilient-fetch.ts:248
// also do NOT call recordSuccess: a 401 sandwiched between
// 5xx responses would otherwise erase the running outage
// signal. The breaker's neutral path leaves state untouched.
breaker.recordNeutral();
return outcome.resp;
case 'terminal-network':
// Either `AbortSignal.timeout()` fired locally OR an external
// caller cancelled the request via AbortController. The server
// never had a chance to answer; this reflects the user's
// network or an explicit cancel, not registry health. Don't
// punish the breaker AND don't reset its outage signal.
breaker.recordNeutral();
throw outcome.err;
case 'retryable-status':
if (attempt + 1 >= retryConfig.maxAttempts) {
breaker.recordFailure();
throw new ResilientFetchExhaustedError(outcome.resp);
}
await sleep(
computeBackoffMs(
attempt,
retryConfig.baseDelayMs,
retryConfig.capDelayMs,
outcome.afterMs,
random,
),
);
break;
case 'retryable-network':
if (attempt + 1 >= retryConfig.maxAttempts) {
breaker.recordFailure();
throw outcome.err;
}
await sleep(View on GitHub (pinned to d540b00184)
Solutions
- Inspect error.response.status and error.response.headers to see the underlying failure (5xx vs 429)
- If 429, honor the server's Retry-After header (cap is 30s) before any external retry
- Increase retry.maxAttempts / capDelayMs in ResilientFetchOptions if the dependency recovers slowly
- Address the upstream cause — a persistent 5xx means the backend is genuinely broken, not transiently flaky
Example fix
// before — caller treats exhaustion as a generic error
try { await resilientFetch(url, init, opts); }
catch (e) { console.log('failed'); }
// after — inspect the carried response for the real status
try { await resilientFetch(url, init, opts); }
catch (e) {
if (e instanceof ResilientFetchExhaustedError) {
console.error('gave up after retries; last status =', e.response.status);
} else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// No pre-check possible — the server decides. But you can size the retry budget:
import type { ResilientFetchOptions } from 'gitnexus-shared/src/integrations/resilient-fetch.js';
const opts: ResilientFetchOptions = {
retry: { maxAttempts: 5, baseDelayMs: 500, capDelayMs: 10_000 },
}; Type guard
import { ResilientFetchExhaustedError } from 'gitnexus-shared/src/integrations/resilient-fetch.js';
function isExhausted(e: unknown): e is ResilientFetchExhaustedError {
return e instanceof ResilientFetchExhaustedError;
} Try / catch
try {
return await resilientFetch(url, init, opts);
} catch (e) {
if (e instanceof ResilientFetchExhaustedError) {
// e.response is the last Response — inspect .status / .headers
if (e.response.status === 429) {
const ra = e.response.headers.get('Retry-After');
// honor server's Retry-After before any external retry
}
throw e;
}
throw e;
} Prevention
- Size retry.maxAttempts and capDelayMs to the dependency's recovery time — too low exhausts on transient blips
- Honor Retry-After on 429 rather than retrying immediately (resilientFetch caps it at RETRY_AFTER_CAP_MS=30s)
- Monitor exhaustion rates — persistent exhaustion indicates a real upstream fault, not transient flakiness
- Distinguish terminal 4xx (no retry, classified terminal-client) from 5xx/429 (retryable) when reasoning about cause
When it happens
Trigger: Every fetch attempt in the retry loop returns a 5xx or 429 (after honoring Retry-After on 429) up to retryConfig.maxAttempts (default 3). The final attempt's response is wrapped and thrown. Does NOT fire for 4xx (other than 429) or terminal network errors — those take different paths.
Common situations: The registry/backend is overloaded (sustained 503), rate-limited (429 with Retry-After exceeding the cap), or throwing 500s due to an internal bug. Also when maxAttempts is too low for the dependency's recovery time, or when Retry-After caps (RETRY_AFTER_CAP_MS default 30000) force premature re-attempts.
Related errors
- LLM endpoint circuit open: retry in ${Math.ceil(err.retryAft
- Circuit '${key}' is open; retry in ${Math.ceil(retryAfterMs
- hf-circuit-open: HuggingFace download circuit is open after
- hf-circuit-open: HuggingFace download circuit opened after 3
- Embedding endpoint returned an unparseable response (${safeU
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/e57b5204a003724d.
Report an issue: GitHub.