{"record":{"id":"e57b5204a003724d","repo":"abhigyanpatwari/GitNexus","slug":"request-failed-after-retries-http-response-stat","errorCode":null,"errorMessage":"Request failed after retries (HTTP ${response.status})","messagePattern":"Request failed after retries \\(HTTP (.+?)\\)","errorType":"exception","errorClass":"ResilientFetchExhaustedError","httpStatus":null,"severity":"error","filePath":"gitnexus-shared/src/integrations/resilient-fetch.ts","lineNumber":248,"sourceCode":"        // also do NOT call recordSuccess: a 401 sandwiched between\n        // 5xx responses would otherwise erase the running outage\n        // signal. The breaker's neutral path leaves state untouched.\n        breaker.recordNeutral();\n        return outcome.resp;\n\n      case 'terminal-network':\n        // Either `AbortSignal.timeout()` fired locally OR an external\n        // caller cancelled the request via AbortController. The server\n        // never had a chance to answer; this reflects the user's\n        // network or an explicit cancel, not registry health. Don't\n        // punish the breaker AND don't reset its outage signal.\n        breaker.recordNeutral();\n        throw outcome.err;\n\n      case 'retryable-status':\n        if (attempt + 1 >= retryConfig.maxAttempts) {\n          breaker.recordFailure();\n          throw new ResilientFetchExhaustedError(outcome.resp);\n        }\n        await sleep(\n          computeBackoffMs(\n            attempt,\n            retryConfig.baseDelayMs,\n            retryConfig.capDelayMs,\n            outcome.afterMs,\n            random,\n          ),\n        );\n        break;\n\n      case 'retryable-network':\n        if (attempt + 1 >= retryConfig.maxAttempts) {\n          breaker.recordFailure();\n          throw outcome.err;\n        }\n        await sleep(","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/gitnexus-shared/src/integrations/resilient-fetch.ts#L230-L266","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before — caller treats exhaustion as a generic error\ntry { await resilientFetch(url, init, opts); }\ncatch (e) { console.log('failed'); }\n\n// after — inspect the carried response for the real status\ntry { await resilientFetch(url, init, opts); }\ncatch (e) {\n  if (e instanceof ResilientFetchExhaustedError) {\n    console.error('gave up after retries; last status =', e.response.status);\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// No pre-check possible — the server decides. But you can size the retry budget:\nimport type { ResilientFetchOptions } from 'gitnexus-shared/src/integrations/resilient-fetch.js';\nconst opts: ResilientFetchOptions = {\n  retry: { maxAttempts: 5, baseDelayMs: 500, capDelayMs: 10_000 },\n};","typeGuard":"import { ResilientFetchExhaustedError } from 'gitnexus-shared/src/integrations/resilient-fetch.js';\nfunction isExhausted(e: unknown): e is ResilientFetchExhaustedError {\n  return e instanceof ResilientFetchExhaustedError;\n}","tryCatchPattern":"try {\n  return await resilientFetch(url, init, opts);\n} catch (e) {\n  if (e instanceof ResilientFetchExhaustedError) {\n    // e.response is the last Response — inspect .status / .headers\n    if (e.response.status === 429) {\n      const ra = e.response.headers.get('Retry-After');\n      // honor server's Retry-After before any external retry\n    }\n    throw e;\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["retry","resilience","http","network"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}