ErrLookup › Background articles › "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause
"API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause
"API request failed" errors appear when a library calls an external HTTP API and the request returns a non-2xx status or never completes — the library wraps the failure in a generic message while embedding (or hiding) the real cause, such as a 401 from an invalid API key, a 400 validation rejection, a 429 rate limit, a 5xx outage, or a network failure that never got a response. This guide explains how to decode the wrapped message and fix the underlying request.
Distilled from 126 documented records across 29 repositories.
Background
This family covers a pattern rather than a single bug: a library or tool makes an HTTP request to an external API (Composio, Groq, Cloudflare Workers AI, Firecrawl, DashScope, Linear, Pulumi Cloud, GitHub/Azure DevOps, Hupu, Bilibili, and more), the request fails at the transport or HTTP layer, and the calling code raises its own error whose message is a generic prefix — 'failed to fetch work items', 'Composio v3 action execution failed', 'Error calling Cloudflare Workers AI API', 'Failed to publish agent flow' — plus whatever detail it decided to attach. The family exists because most SDK authors want one readable message at the point of failure, but the result is that the developer's real diagnostic work happens inside the message text.
How much survives the wrapping varies enormously by library. Some preserve the useful parts deliberately: zeroclaw's response_error() embeds the HTTP status code plus the API's own error.message with sensitive IDs redacted and the text truncated to 240 chars; vercel/ai includes 'Google realtime auth token request failed: <status> <body>'; AnythingLLM's image generator appends the provider's response body or statusText; dawarich's Rails API puts the exact service failure in a JSON error field. Others destroy the information: litellm's KeysManagementClient.update() collapses every failure — including 401s that elsewhere become typed UnauthorizedError — into a bare Exception whose only clue is the raw response body (or the literal 'None' when the request never got a response), and discards the original traceback by raising without a cause. context7's 'Please try again' is the extreme case: it appears precisely when the response carries no message field, masking a network error, auth failure, or rate limit entirely.
From the caller's side, the first skill is reading the suffix. In most of these libraries the actionable detail is whatever comes after the prefix: an HTTP status code, the provider's error string ('Invalid API Key', 'model_decommissioned', 'Throttling'), a JSON detail field, or the literal word 'None'/'unknown error' meaning no response ever arrived. A second pattern is nesting: some libraries wrap each other's wrappers, so a message like 'DashScope QwenVLClient error: DashScope QwenVLClient failed: ...' requires reading the inner layer to learn the API returned a non-200 status, and the outer layer to learn where it was caught.
The status code, when present, is the reliable classifier, and the family's own records converge on the same triage: 401/403 mean the credential is wrong, revoked, or lacks scope; 400/404/422 mean your inputs are wrong (a bad tool slug, a nonexistent key hash, an unparseable timestamp, a model without the endpoint you called); 429 and 5xx are the only transient cases worth retrying, ideally with exponential backoff; and a literal 'None', 'fetch failed', or connection error means the problem is local egress — DNS, proxy, firewall, or the service being down. Note that some behavior is library-specific: whether a 401 is retried automatically (BloopAI's authenticatedFetch refreshes once before giving up), whether non-JSON error bodies throw a parsing error before the status is even checked (dawarich's client-side response.json() ordering), and whether errors surface as exceptions or as structured results (mastra converts scrape failures into an isError MCP result rather than throwing).
Common causes
- Invalid, expired, or revoked API key. 401/403 responses from a mistyped, expired, or revoked credential are the most consistent trigger across the family — Composio keys, GROQ_API_KEY, Firecrawl and DashScope keys, Linear and Pulumi tokens, PATs without required scope. The fix is almost always re-authenticating and verifying with a cheap read-only call before retrying real work.
- Wrong identifier or input rejected by the server. 400/404/422 responses: a nonexistent key hash or alias, an invalid toolkit slug, a decommissioned model id, a duplicate or invalid visit payload, malformed session config, or IDs referencing deleted resources. The embedded body or 'detail' field names the exact validation the server rejected.
- Rate limiting (429). Bursty agent loops, batch syncs, and parallel fan-out hit provider rate limits. Most wrappers make a single attempt with no built-in retry, so the caller must back off exponentially and reduce concurrency.
- Provider-side outage (5xx). The upstream service fails or returns HTML error pages instead of JSON, which can also cause downstream JSON parse failures in wrappers that unconditionally parse the body. Check the provider's status page and retry later.
- Network/egress failure — the request never got a response. DNS failure, refused connections, proxies, and unreachable hosts produce the least informative variants: litellm's 'Error updating key: None', chroma's 'fetch failed', OpenCLI's wrapped transport errors. Verify egress from the failing host with curl and check proxy environment variables.
- Session or token expired mid-operation. Long-running processes outlive their credentials: Pulumi tokens in CI, reflex hosting tokens during long scripts, browser sessions needed for signed API calls (OpenCLI's Wbi signing), or refresh tokens revoked mid-wait (BloopAI, grafana/k6 polling). Refresh credentials before long operations.
- Stale local state acting on changed server state. Acting on cached data the server has already changed — claiming an already-completed work item, executing against an expired OAuth connected account, fetching deleted work-item IDs — produces 4xx rejections whose messages are actually correct behavior, not bugs.
What usually fixes it
- Decode the message before changing anything: read the embedded HTTP status, the provider's error body after the prefix, and any nested layers. The classification is 401/403 = credentials, 400/404/422 = your inputs, 429/5xx = transient, 'None'/connection text = no response at all.
- Retry only what is transient. Exponential backoff is appropriate for 429 and 5xx (and pure network failures); treat 401/403 and 4xx validation errors as permanent and fix the configuration or inputs instead. Several records explicitly warn against auto-retrying side-effecting actions without idempotency knowledge.
- Verify credentials and reachability cheaply before retrying the expensive call: a list/probe/whoami-style request confirms the key, and a curl from the failing host confirms egress, DNS, and proxy settings.
- Pre-validate inputs client-side: exact identifiers (slugs, key hashes not aliases, deployment IDs, model names), required fields, payload sizes, and parseable timestamps — most 400/422s in this family are preventable before the request is sent.
- Refresh credentials before long-running operations and don't assume the library will re-authenticate for you; where it does (one automatic refresh after 401), a second 401 means the session is unrecoverable and you should re-login.
- Work around the wrapper where it destroys information: catch the generic exception narrowly and re-raise a typed error, log response bodies and error.cause yourself, and check ok/status fields in structured responses rather than trusting the thrown message.
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
- DNS resolution errors: ENOTFOUND and getaddrinfo failures — how hostname lookups fail and how to debug them.
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Documented occurrences
- Composio v3 connected accounts lookup failed: {err} (zeroclaw-labs/zeroclaw)
- Error updating key: {response_text} (BerriAI/litellm)
- Please try again (upstash/context7)
- Error calling Cloudflare Workers AI API: ${error.message} (chroma-core/chroma)
- errorData.error || `Failed to ${isEdit ? "update" : "create"} visit` (Freika/dawarich)
- Composio v3 action execution failed: {err} (zeroclaw-labs/zeroclaw)
- e.message (Mintplex-Labs/anything-llm)
- Failed to create visit (Freika/dawarich)
- Error calling Cloudflare Workers AI API: ${error} (chroma-core/chroma)
- failed to fetch work items: %w (gastownhall/beads)
- Read Hupu mentions failed: ${result.error || 'unknown error'} (jackwener/OpenCLI)
- DashScope QwenVLClient failed: {getattr(response, 'message', response)} (ATH-MaaS/Pixelle-Video)
- 获取视频信息失败: ${err?.message || err} (jackwener/OpenCLI)
- DashScope QwenVLClient error: {e} (ATH-MaaS/Pixelle-Video)
- Failed to list organizations (${res.status}) (BloopAI/vibe-kanban)
- Image edit failed (${res.status}): ${body || res.statusText} (Mintplex-Labs/anything-llm)
- Composio v3 auth config lookup failed: {err} (zeroclaw-labs/zeroclaw)
- 获取视频播放信息失败: ${err?.message || err} (jackwener/OpenCLI)
- fetching test status: %w (grafana/k6)
- Failed to like video (jackwener/OpenCLI)
…and 106 more across the corpus — use search.
Honest provenance: generated on 2026-08-31 from AI-assisted analysis of the linked records. See how records are made.