{"record":{"id":"4f80ebd889d68828","repo":"can1357/oh-my-pi","slug":"exa-api-error-response-status-errortext","errorCode":null,"errorMessage":"Exa API error (${response.status}): ${errorText}","messagePattern":"Exa API error \\((.+?)\\): (.+?)","errorType":"exception","errorClass":"SearchProviderError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/web/search/providers/exa.ts","lineNumber":329,"sourceCode":"\tconst body = buildExaRequestBody(params);\n\n\tconst fetchImpl = params.fetch ?? fetch;\n\tawait waitForExaSearchSlot(params.signal);\n\tconst response = await fetchImpl(EXA_API_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\"x-api-key\": apiKey,\n\t\t},\n\t\tbody: JSON.stringify(body),\n\t\tsignal: withHardTimeout(params.signal, params.timeoutMs),\n\t});\n\n\tif (!response.ok) {\n\t\tconst errorText = await response.text();\n\t\tconst classified = classifyProviderHttpError(\"exa\", response.status, errorText);\n\t\tif (classified) throw classified;\n\t\tthrow new SearchProviderError(\"exa\", `Exa API error (${response.status}): ${errorText}`, response.status);\n\t}\n\n\treturn response.json() as Promise<ExaSearchResponse>;\n}\nfunction buildExaMcpArgs(params: ExaSearchParams): Record<string, unknown> {\n\tconst queryParts = [params.query];\n\tfor (const domain of params.include_domains ?? []) {\n\t\tconst trimmed = domain.trim();\n\t\tif (trimmed) queryParts.push(`site:${trimmed}`);\n\t}\n\tfor (const domain of params.exclude_domains ?? []) {\n\t\tconst trimmed = domain.trim();\n\t\tif (trimmed) queryParts.push(`-site:${trimmed}`);\n\t}\n\tif (params.start_published_date) queryParts.push(`after:${params.start_published_date}`);\n\tif (params.end_published_date) queryParts.push(`before:${params.end_published_date}`);\n\n\tconst args: Record<string, unknown> = { query: queryParts.join(\" \") };","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/web/search/providers/exa.ts#L311-L347","documentation":"The Exa REST API returned a non-2xx response. The provider reads the response body as text, attempts classification into a specific error, and if none applies throws SearchProviderError with the raw status and the response body text embedded.","triggerScenarios":"Any authenticated callExaSearch REST request where response.ok is false: invalid API key (401), quota exceeded (429), bad request parameters (400), or Exa server errors (5xx) that the classifier doesn't special-case.","commonSituations":"Expired or wrong EXA_API_KEY; malformed query parameters sent to Exa; Exa outage; exceeding paid-plan limits with unclassified body text.","solutions":["Read the embedded errorText — Exa's JSON error body usually states the exact problem.","Verify EXA_API_KEY is valid and active at dashboard.exa.ai (401/403).","Check request parameters (query, numResults, category) against Exa's API docs (400).","Retry with backoff for 429/5xx; check rate limits on your Exa plan.","Fall back to the keyless Exa MCP path or another provider."],"exampleFix":"// before\nconst res = await searchExa({ query, apiKey });\n// after\ntry {\n  const res = await searchExa({ query, apiKey });\n} catch (e) {\n  console.error(e.message); // e.g. \"Exa API error (401): {\\\"error\\\":\\\"invalid api key\\\"}\"\n}","handlingStrategy":"try-catch","validationCode":"if (!process.env.EXA_API_KEY) throw new Error(\"EXA_API_KEY not set; Exa REST calls will fail\");","typeGuard":null,"tryCatchPattern":"try {\n  const res = await searchExa({ query, apiKey });\n} catch (e) {\n  if (e instanceof SearchProviderError && e.provider === \"exa\") {\n    // e.status + embedded errorText tell you 401 (bad key) vs 429 (quota) vs 5xx\n    if (e.status === 401) refreshApiKey();\n    else if (e.status === 429) backoffAndRetry();\n  } else throw e;\n}","preventionTips":["Validate EXA_API_KEY before making calls (probe once at startup)","Match parameters against Exa's API docs to avoid 400s","Back off on 429/5xx instead of hammering","Read the embedded errorText — Exa returns descriptive JSON error bodies"],"tags":["api","http","exa","search-provider","auth"],"backgroundTag":"api-error-response","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}