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

  1. Inspect error.response.status and error.response.headers to see the underlying failure (5xx vs 429)
  2. If 429, honor the server's Retry-After header (cap is 30s) before any external retry
  3. Increase retry.maxAttempts / capDelayMs in ResilientFetchOptions if the dependency recovers slowly
  4. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/e57b5204a003724d. Report an issue: GitHub.