{"record":{"id":"de57b7aab73acfb5","repo":"decolua/9router","slug":"providerid-errormsg","errorCode":null,"errorMessage":"[${providerId}] ${errorMsg}","messagePattern":"\\[\\$\\{providerId\\}\\] \\$\\{errorMsg\\}","errorType":"http","errorClass":null,"httpStatus":503,"severity":"warning","filePath":"src/sse/handlers/fetch.js","lineNumber":168,"sourceCode":"        headers: { \"Content-Type\": \"application/json\", \"Access-Control-Allow-Origin\": \"*\" }\n      });\n    }\n    return errorResponse(result.status || HTTP_STATUS.BAD_GATEWAY, result.error || \"Fetch failed\");\n  }\n\n  // Credential + fallback loop\n  const excludeConnectionIds = new Set();\n  let lastError = null;\n  let lastStatus = null;\n\n  while (true) {\n    const credentials = await getProviderCredentials(providerId, excludeConnectionIds);\n\n    if (!credentials || credentials.allRateLimited) {\n      if (credentials?.allRateLimited) {\n        const errorMsg = lastError || credentials.lastError || \"Unavailable\";\n        const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE;\n        log.warn(\"FETCH\", `[${providerId}] ${errorMsg} (${credentials.retryAfterHuman})`);\n        return unavailableResponse(status, `[${providerId}] ${errorMsg}`, credentials.retryAfter, credentials.retryAfterHuman);\n      }\n      if (excludeConnectionIds.size === 0) {\n        log.error(\"AUTH\", `No credentials for provider: ${providerId}`);\n        return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${providerId}`);\n      }\n      log.warn(\"FETCH\", \"No more accounts available\", { provider: providerId });\n      return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || \"All accounts unavailable\");\n    }\n\n    log.info(\"AUTH\", `\\x1b[32mUsing ${providerId} account: ${credentials.connectionName}\\x1b[0m`);\n\n    const refreshedCredentials = await checkAndRefreshToken(providerId, credentials);\n\n    const result = await handleFetchCore({\n      url: targetUrl,\n      format,\n      maxCharacters,","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/sse/handlers/fetch.js#L150-L186","documentation":"This is a 503-style 'provider temporarily unavailable' response raised in handleSingleProviderFetch (src/sse/handlers/fetch.js:168). It fires when getProviderCredentials reports credentials.allRateLimited for the requested provider, meaning every connection for that provider is inside a rate-limit/unavailable lock window. The message embeds the last upstream error (or 'Unavailable') plus a Retry-After hint (credentials.retryAfterHuman) so the caller knows when to come back. The gateway deliberately refuses to keep hammering a provider whose accounts have all been throttled by the upstream API.","triggerScenarios":"POST /v1/fetch (web fetch) where the resolved provider has at least one stored connection, but all of its connections are currently marked rate-limited/unavailable: a previous fetch/chat call got a 429 (or another lock-triggering status) from the upstream, markAccountUnavailable set the account-wide lock, and now getProviderCredentials returns { allRateLimited: true, lastError, lastErrorCode, retryAfter }.","commonSituations":"Bursting a web-fetch pipeline past the upstream provider's quota (e.g. many URL extractions in a loop through one provider); sharing a single free-tier API key across multiple tools; a provider outage that returns 429/5xx for every account, tripping the lock for the whole retry window; stale lastErrorCode from an earlier failure keeping the 503 status sticky.","solutions":["Wait until the Retry-After timestamp in the response (credentials.retryAfterHuman) elapses, then retry the request.","Check the dashboard's provider/connections page for the rate-limited account and clear the error state manually.","Add more connections (additional API keys/accounts) for that provider so multi-account fallback has another credential to use.","Route the fetch through a combo (multiple providers) so handleComboChat can fall back to a different provider instead of hitting the locked one.","Reduce request concurrency against the provider or add client-side backoff to avoid re-tripping the 429 lock."],"exampleFix":"// before: tight loop that exhausts the provider and trips the lock\nfor (const url of urls) {\n  await fetch(`${base}/v1/fetch`, { method: 'POST', body: JSON.stringify({ provider: 'exa', url }) });\n}\n// after: respect Retry-After and back off\nlet res;\nfor (const url of urls) {\n  res = await fetch(`${base}/v1/fetch`, { method: 'POST', body: JSON.stringify({ provider: 'exa', url }) });\n  if (res.status === 503) {\n    const retryAfter = Number(res.headers.get('Retry-After')) || 60;\n    await new Promise(r => setTimeout(r, retryAfter * 1000));\n  }\n}","handlingStrategy":"retry","validationCode":"// Cannot be fully pre-checked (server-side lock state), but check the response up front:\nconst res = await fetch(base + '/v1/fetch', { ...opts });\nif (res.status === 503) {\n  const ra = Number(res.headers.get('Retry-After'));\n  if (ra) await new Promise(r => setTimeout(r, ra * 1000));\n}","typeGuard":null,"tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  const res = await doFetch();\n  if (res.status !== 503) return res;\n  const retryAfter = Number(res.headers.get('Retry-After')) || 30 * (attempt + 1);\n  await new Promise(r => setTimeout(r, retryAfter * 1000));\n}\nthrow new Error('Provider rate-limited after retries');","preventionTips":["Add client-side throttling/backoff so you never exceed the provider's quota in the first place.","Configure multiple connections per provider so the gateway can rotate accounts.","Use combos that span distinct providers for automatic cross-provider fallback.","Monitor the dashboard for accounts entering rate-limited state and rotate keys proactively."],"tags":["rate-limit","provider-unavailable","web-fetch","credentials"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}