{"record":{"id":"09e04f2adfdc9fce","repo":"koala73/worldmonitor","slug":"exa-endpoint-slice-1-failed-http-response-st","errorCode":null,"errorMessage":"Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${detail.slice(0, 120)}","messagePattern":"Exa (.+?) failed HTTP (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"consumer-prices-core/src/acquisition/exa.ts","lineNumber":141,"sourceCode":"      return false;\n    }\n  }\n\n  private async request<T>(endpoint: string, body: Record<string, unknown>, timeout = 30_000): Promise<T> {\n    const response = await globalThis.fetch(`${this.baseUrl}${endpoint}`, {\n      method: 'POST',\n      headers: {\n        'x-api-key': this.apiKey,\n        'Content-Type': 'application/json',\n        'User-Agent': 'worldmonitor-consumer-prices/1.0',\n      },\n      body: JSON.stringify(body),\n      signal: AbortSignal.timeout(timeout),\n    });\n\n    if (!response.ok) {\n      const detail = await response.text().catch(() => '');\n      throw new Error(`Exa ${endpoint.slice(1)} failed HTTP ${response.status}: ${detail.slice(0, 120)}`);\n    }\n\n    return (await response.json()) as T;\n  }\n}\n\nfunction parseStructuredSummary<T>(summary: unknown): T | null {\n  if (summary && typeof summary === 'object') return summary as T;\n  if (typeof summary !== 'string') return null;\n\n  const trimmed = summary.trim().replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '');\n  if (!trimmed) return null;\n  try {\n    const parsed: unknown = JSON.parse(trimmed);\n    return parsed && typeof parsed === 'object' ? (parsed as T) : null;\n  } catch {\n    return null;\n  }","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/acquisition/exa.ts#L123-L159","documentation":"All Exa REST calls in this provider (search, contents) go through request(), which throws on any non-2xx with the endpoint name, HTTP status, and the first 120 characters of the response body — enough to distinguish 401 (bad API key), 400 INVALID_REQUEST_BODY, 429 rate/quota, and 5xx incidents.","triggerScenarios":"401 from a missing or typo'd Exa API key; 400 when the summary schema uses a form Exa's validator rejects — notably JSON-Schema type arrays like ['number','null'] instead of anyOf; 429 rate limit or exhausted credits during burst scraping; 5xx provider incidents.","commonSituations":"API key rotated but the env var not updated; schema evolution introducing nullable union types; a large scrape run draining the plan quota mid-batch.","solutions":["Read the status and body slice in the message: 401 → fix the EXA API key env; 400 → fix the request/schema shape","For nullable fields use anyOf: [{ type: 'number' }, { type: 'null' }] — never type arrays","On 429, back off exponentially and check the plan quota before resuming the batch","On 5xx, retry with backoff and check the Exa status page"],"exampleFix":"// before — nullable field as a JSON-Schema type array\nconst outputSchema = {\n  type: 'object',\n  properties: { price: { type: ['number', 'null'] } },  // Exa 400: INVALID_REQUEST_BODY\n};\n\n// after — express nullability with anyOf, which Exa's validator accepts\nconst outputSchema = {\n  type: 'object',\n  properties: { price: { anyOf: [{ type: 'number' }, { type: 'null' }] } },\n};","handlingStrategy":"try-catch","validationCode":"// Cheap preflight: key present and non-empty before any Exa call\nif (!process.env.EXA_API_KEY) throw new Error('EXA_API_KEY not set');","typeGuard":null,"tryCatchPattern":"try {\n  await exa.search(q);\n} catch (err) {\n  const m = /failed HTTP (\\d+): (.*)/.exec(err.message);\n  if (!m) throw err;\n  const [, status, detail] = m;\n  if (status === '401') throw new ConfigError('Exa API key invalid');\n  if (status === '429') return backoffRetry(fn);            // rate/quota — retryable\n  if (Number(status) >= 500) return backoffRetry(fn);       // provider incident\n  throw new Error(`Exa request rejected: ${detail}`);        // 400 — fix the payload\n}","preventionTips":["Fail fast on missing API keys; surface 401 as config, not runtime, errors","Validate schema shapes against Exa's validator (anyOf for nullability) before sending","Back off on 429 and monitor quota consumption per batch"],"tags":["exa","http","api-errors","rate-limit"],"backgroundTag":"http-request-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}