decolua/9router · warning

[${providerId}] ${errorMsg}

Error message

[${providerId}] ${errorMsg}

What it means

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.

Source

Thrown at src/sse/handlers/fetch.js:168

        headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
      });
    }
    return errorResponse(result.status || HTTP_STATUS.BAD_GATEWAY, result.error || "Fetch failed");
  }

  // Credential + fallback loop
  const excludeConnectionIds = new Set();
  let lastError = null;
  let lastStatus = null;

  while (true) {
    const credentials = await getProviderCredentials(providerId, excludeConnectionIds);

    if (!credentials || credentials.allRateLimited) {
      if (credentials?.allRateLimited) {
        const errorMsg = lastError || credentials.lastError || "Unavailable";
        const status = lastStatus || Number(credentials.lastErrorCode) || HTTP_STATUS.SERVICE_UNAVAILABLE;
        log.warn("FETCH", `[${providerId}] ${errorMsg} (${credentials.retryAfterHuman})`);
        return unavailableResponse(status, `[${providerId}] ${errorMsg}`, credentials.retryAfter, credentials.retryAfterHuman);
      }
      if (excludeConnectionIds.size === 0) {
        log.error("AUTH", `No credentials for provider: ${providerId}`);
        return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${providerId}`);
      }
      log.warn("FETCH", "No more accounts available", { provider: providerId });
      return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || "All accounts unavailable");
    }

    log.info("AUTH", `\x1b[32mUsing ${providerId} account: ${credentials.connectionName}\x1b[0m`);

    const refreshedCredentials = await checkAndRefreshToken(providerId, credentials);

    const result = await handleFetchCore({
      url: targetUrl,
      format,
      maxCharacters,

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Wait until the Retry-After timestamp in the response (credentials.retryAfterHuman) elapses, then retry the request.
  2. Check the dashboard's provider/connections page for the rate-limited account and clear the error state manually.
  3. Add more connections (additional API keys/accounts) for that provider so multi-account fallback has another credential to use.
  4. Route the fetch through a combo (multiple providers) so handleComboChat can fall back to a different provider instead of hitting the locked one.
  5. Reduce request concurrency against the provider or add client-side backoff to avoid re-tripping the 429 lock.

Example fix

// before: tight loop that exhausts the provider and trips the lock
for (const url of urls) {
  await fetch(`${base}/v1/fetch`, { method: 'POST', body: JSON.stringify({ provider: 'exa', url }) });
}
// after: respect Retry-After and back off
let res;
for (const url of urls) {
  res = await fetch(`${base}/v1/fetch`, { method: 'POST', body: JSON.stringify({ provider: 'exa', url }) });
  if (res.status === 503) {
    const retryAfter = Number(res.headers.get('Retry-After')) || 60;
    await new Promise(r => setTimeout(r, retryAfter * 1000));
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Cannot be fully pre-checked (server-side lock state), but check the response up front:
const res = await fetch(base + '/v1/fetch', { ...opts });
if (res.status === 503) {
  const ra = Number(res.headers.get('Retry-After'));
  if (ra) await new Promise(r => setTimeout(r, ra * 1000));
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  const res = await doFetch();
  if (res.status !== 503) return res;
  const retryAfter = Number(res.headers.get('Retry-After')) || 30 * (attempt + 1);
  await new Promise(r => setTimeout(r, retryAfter * 1000));
}
throw new Error('Provider rate-limited after retries');

Prevention

When it happens

Trigger: 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 }.

Common situations: 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.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/de57b7aab73acfb5. Report an issue: GitHub.