{"record":{"id":"952cdcf04f8c67a1","repo":"farion1231/cc-switch","slug":"http-response-status","errorCode":null,"errorMessage":"HTTP ${response.status}","messagePattern":"HTTP \\$\\{response\\.status\\}","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/modelsDevPricing.ts","lineNumber":152,"sourceCode":"    (a, b) =>\n      b.releaseDate.localeCompare(a.releaseDate) ||\n      a.modelName.localeCompare(b.modelName),\n  );\n  return entries;\n}\n\nexport async function fetchModelsDevPricing(): Promise<ModelsDevResponse> {\n  const controller = new AbortController();\n  const timeout = window.setTimeout(\n    () => controller.abort(),\n    MODELS_DEV_FETCH_TIMEOUT_MS,\n  );\n  try {\n    const response = await fetch(MODELS_DEV_API_URL, {\n      signal: controller.signal,\n    });\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}`);\n    }\n    return (await response.json()) as ModelsDevResponse;\n  } finally {\n    window.clearTimeout(timeout);\n  }\n}\n\nconst COMMON_MODEL_LIMIT_PER_FAMILY = 6;\n\ninterface CommonFamilyRule {\n  id: string;\n  providers: ReadonlySet<string>;\n  matches: (modelId: string) => boolean;\n}\n\nconst COMMON_FAMILY_RULES: CommonFamilyRule[] = [\n  {\n    id: \"claude\",","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/farion1231/cc-switch/blob/0b5da510168914b251481654a568c3ffacd62cf4/src/lib/modelsDevPricing.ts#L134-L170","documentation":"fetchModelsDevPricing fetches the models.dev pricing catalog with an AbortController-bounded timeout (MODELS_DEV_FETCH_TIMEOUT_MS) and throws 'HTTP <status>' whenever response.ok is false. The message intentionally carries only the status code; a timeout instead surfaces as an AbortError from fetch, not as this error. This is a transient server-side failure class: 5xx outages and 429 rate limits are the usual statuses.","triggerScenarios":"fetch(MODELS_DEV_API_URL) resolves with response.ok === false — e.g. models.dev returning 502/503 behind its CDN, or 429 when the client retries too aggressively.","commonSituations":"App startup while models.dev is degraded; CI/e2e runs hitting rate limits; corporate proxies returning 403 interstitial pages.","solutions":["Retry with exponential backoff (especially for 429/5xx) before giving up","Cache the last successful ModelsDevResponse and serve it when the fetch fails","Degrade gracefully — pricing is auxiliary data, so keep provider switching working without it"],"exampleFix":"// before\nconst data = await fetchModelsDevPricing(); // throws 'HTTP 503'\n\n// after\nlet data: ModelsDevResponse | null = null;\nfor (let attempt = 0; attempt < 3 && !data; attempt++) {\n  try {\n    data = await fetchModelsDevPricing();\n  } catch (error) {\n    if (attempt === 2) {\n      data = lastGoodCache; // fall back to last good copy\n    } else {\n      await delay(2 ** attempt * 500);\n    }\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function fetchPricingSafe(fallback: ModelsDevResponse | null): Promise<ModelsDevResponse | null> {\n  for (let attempt = 0; attempt < 3; attempt++) {\n    try {\n      return await fetchModelsDevPricing();\n    } catch (error) {\n      const msg = error instanceof Error ? error.message : '';\n      const retriable = msg.startsWith('HTTP 5') || msg === 'HTTP 429';\n      if (!retriable || attempt === 2) return fallback; // degrade to cache/offline\n      await new Promise((r) => setTimeout(r, 2 ** attempt * 500));\n    }\n  }\n  return fallback;\n}","preventionTips":["Cache the last good pricing payload with a timestamp","Treat pricing as optional data so the app works offline","Only retry 429/5xx; fail fast on other statuses"],"tags":["network","http","pricing","models-dev"],"backgroundTag":"http-error-response","analyzedSha":"0b5da510168914b251481654a568c3ffacd62cf4","analyzedAt":"2026-08-20T14:29:00.113Z","contentChangedAt":"2026-08-20T14:29:00.113Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}