{"record":{"id":"062819572cacc109","repo":"upstash/context7","slug":"errorbody-error-errorbody-message-res-status","errorCode":null,"errorMessage":"errorBody.error || errorBody.message || res.statusText","messagePattern":"errorBody\\.error \\|\\| errorBody\\.message \\|\\| res\\.statusText","errorType":"http","errorClass":"Context7Error","httpStatus":null,"severity":"error","filePath":"packages/sdk/src/http/index.ts","lineNumber":176,"sourceCode":"        if (requestOptions.signal?.aborted) {\n          throw error_;\n        }\n        error = error_ as Error;\n        if (i < this.retry.attempts) {\n          await new Promise((r) => setTimeout(r, this.retry.backoff(i)));\n        }\n      }\n    }\n    if (!res) {\n      throw error ?? new Error(\"Exhausted all retries\");\n    }\n\n    if (!res.ok) {\n      const errorBody = (await res.json().catch(() => ({}))) as {\n        error?: string;\n        message?: string;\n      };\n      throw new Context7Error(errorBody.error || errorBody.message || res.statusText);\n    }\n\n    const contentType = res.headers.get(\"content-type\");\n\n    if (contentType?.includes(\"application/json\")) {\n      const body = await res.json();\n      return { result: body as TResult };\n    } else {\n      const text = await res.text();\n      const headers = this.extractTxtResponseHeaders(res.headers);\n      return { result: text as TResult, headers };\n    }\n  }\n\n  private extractTxtResponseHeaders(headers: Headers): TxtResponseHeaders | undefined {\n    const page = headers.get(\"x-context7-page\");\n    const limit = headers.get(\"x-context7-limit\");\n    const totalPages = headers.get(\"x-context7-total-pages\");","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/upstash/context7/blob/ca15df0443ee770506fc4eb270d1efc71d483933/packages/sdk/src/http/index.ts#L158-L194","documentation":"Thrown by HttpClient.request() when the final response (after retrying only fetch-level rejections) is non-ok. It parses the body as JSON and throws Context7Error with errorBody.error, errorBody.message, or res.statusText as the message. Important: the retry loop only retries when fetch() throws (network errors); HTTP non-ok responses are NOT retried, so 429/5xx surface immediately.","triggerScenarios":"401/403 (invalid/expired API key), 404 (wrong endpoint or path), 429 (rate limited — not retried), 400 (malformed query params), 5xx (server error). The message is the server's `error`/`message` field if present, otherwise the bare HTTP statusText (e.g. 'Too Many Requests').","commonSituations":"Wrong API key or one missing the 'ctx7sk' prefix that the server rejected; query/libraryName params the server rejected; exceeded the plan's rate limit; transient 5xx during an incident; pointed the SDK at a wrong base URL returning 404.","solutions":["Inspect the thrown message: if it is plain statusText, curl the endpoint to see the full body for a more specific error.","401/403 → regenerate the API key from the dashboard and confirm it starts with 'ctx7sk'.","429 → reduce request rate / add backoff; note the SDK does not auto-retry HTTP 429.","5xx → retry with exponential backoff at the call site; if persistent, check the status page."],"exampleFix":"// before — single shot, non-ok is fatal\nconst libs = await client.exec(new SearchLibraryCommand(q, lib));\n\n// after — wrap and branch on status surfaced via the message\ntry {\n  const libs = await client.exec(new SearchLibraryCommand(q, lib));\n} catch (e) {\n  if (e instanceof Context7Error && /429|Too Many Requests/.test(e.message)) {\n    await sleep(backoffMs); // caller-managed backoff for HTTP 429\n    return retry();\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// The SDK does not retry HTTP non-ok (4xx/5xx); budget for it at the call site.\nasync function callWithHttpRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {\n  for (let i = 0; ; i++) {\n    try {\n      return await fn();\n    } catch (e) {\n      const transient = e instanceof Context7Error &&\n        (/\\b5\\d\\d\\b|internal|bad gateway|service unavailable|gateway timeout/i.test(e.message));\n      if (!transient || i >= retries) throw e;\n      await new Promise((r) => setTimeout(r, Math.min(1000 * 2 ** i, 8000)));\n    }\n  }\n}","typeGuard":"import { Context7Error } from \"@error\";\nfunction isContext7Error(e: unknown): e is Context7Error {\n  return e instanceof Error && (e as Context7Error).name === \"Context7Error\";\n}\nfunction isRateLimited(e: unknown): boolean {\n  return isContext7Error(e) && /429|too many requests/i.test(e.message);\n}","tryCatchPattern":"try {\n  const libs = await client.exec(new SearchLibraryCommand(q, lib));\n} catch (e) {\n  if (isContext7Error(e)) {\n    if (isRateLimited(e)) await sleep(2000);          // HTTP 429 — caller handles backoff\n    else if (/^4\\d\\d|unauthor|forbidden|not found/i.test(e.message)) throw new Error(`Fix request: ${e.message}`);\n    else await sleep(1000);                             // assume transient 5xx\n    return retry();\n  }\n  throw e;\n}","preventionTips":["Remember the SDK retries only fetch rejections, not HTTP non-ok — implement 429/5xx backoff yourself.","Inspect the thrown message to distinguish auth (401/403) from rate-limit (429) from server (5xx) errors.","Keep the API key current and correctly prefixed ('ctx7sk') to avoid avoidable 401s.","When debugging, curl the endpoint directly to read the full response body the SDK distilled into one message."],"tags":["http","sdk","api","retry","error-body"],"backgroundTag":null,"analyzedSha":"ca15df0443ee770506fc4eb270d1efc71d483933","analyzedAt":"2026-08-12T13:31:48.440Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}