{"record":{"id":"27aacbcde1969db9","repo":"JuliusBrussee/caveman","slug":"await-response-text","errorCode":null,"errorMessage":"await response.text()","messagePattern":"await response\\.text\\(\\)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sdk/typescript/src/index.ts","lineNumber":2039,"sourceCode":"  if (target.origin !== base.origin || (target.pathname !== prefix && !target.pathname.startsWith(`${prefix}/`))) {\n    throw new Error(\"cave_provider_raw_path_not_allowed\");\n  }\n  const merged = new Headers(request.headers);\n  const gatewayHeaders = headers(cave, cave.options.defaultWorkflow ?? \"unlabeled-workflow\", upstreamKey);\n  for (const [name, value] of Object.entries(gatewayHeaders)) {\n    if (name !== \"content-type\") merged.set(name, value);\n  }\n  return caveFetch(cave, new Request(request, { headers: merged }));\n}\n\nasync function providerFetch(cave: Cave, path: string, body: unknown, workflow: string, hint?: Record<string, unknown>, upstreamKey?: string, trace?: TraceContext) {\n  const assemblyHeader = body !== null && typeof body === \"object\" ? assemblyRequestHeaders.get(body as object) : undefined;\n  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, {\n    method: \"POST\",\n    headers: headers(cave, workflow, upstreamKey, assemblyHeader ? { ...hint, assemblyHeader } : hint, trace),\n    body: JSON.stringify(body)\n  });\n  if (!response.ok) throw new Error(await response.text());\n  return response.json();\n}\n\nexport class CaveRequestError extends Error {\n  constructor(readonly status: number, readonly path: string, message: string) {\n    super(message);\n    this.name = \"CaveRequestError\";\n  }\n}\n\nasync function request(cave: Cave, path: string, body?: unknown, workflow?: string, trace?: TraceContext, extraHeaders?: Record<string, string>): Promise<Record<string, unknown>> {\n  const init: RequestInit = {\n    method: body === undefined ? \"GET\" : \"POST\",\n    headers: { ...headers(cave, workflow ?? cave.options.defaultWorkflow ?? \"unlabeled-workflow\", undefined, undefined, trace), ...extraHeaders }\n  };\n  if (body !== undefined) init.body = JSON.stringify(body);\n  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, init);\n  if (!response.ok) throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);","sourceCodeStart":2021,"sourceCodeEnd":2057,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/sdk/typescript/src/index.ts#L2021-L2057","documentation":"Raised by the provider convenience clients (responses.create, chat.completions.create, messages) when the proxied call fails: the thrown Error's message is the raw response body text, not a structured error. Since the gateway forwards upstream provider traffic, that body is usually the upstream provider's JSON error — parse it yourself to get error.code/type.","triggerScenarios":"Invalid or missing upstream key (upstream 401 — the gateway forwards it in x-cave-upstream-key only when you passed config.upstreamKey to cave.openai(...)/anthropic(...)); 400 model not in the workflow's allowlist; 429 upstream quota/rate limits; 413 oversized payload; 5xx provider outage propagated through the gateway.","commonSituations":"Forgetting to pass the provider key when constructing the provider client; exhausting provider quota mid-run; a workflow policy change removing a model; provider regional incidents surfacing as opaque text errors.","solutions":["JSON.parse the error message in a catch block to read the upstream error.code/message (see tryCatchPattern)","Verify you constructed the client with the upstream key: cave.openai({ upstreamKey }) — the Cave apiKey alone is not the provider credential","For 429, retry with exponential backoff; for 400/403 model errors, check the workflow's model policy","Reproduce with the same body via .raw() to see full response headers"],"exampleFix":"// before\nconst completion = await cave.openai({ upstreamKey }).chat.completions.create(body); // rejects with raw body text\n\n// after\ntry {\n  const completion = await cave.openai({ upstreamKey }).chat.completions.create(body);\n} catch (e) {\n  let upstream: { error?: { code?: string; message?: string } } = {};\n  try { upstream = JSON.parse((e as Error).message); } catch { /* non-JSON body */ }\n  console.error(\"provider error:\", upstream.error?.code, upstream.error?.message);\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"function assertProviderClientConfigured(cave: Cave, upstreamKey: string | undefined): void {\n  if (!upstreamKey) throw new Error(\"upstreamKey missing — provider calls will fail at the gateway\");\n}","typeGuard":"function parseUpstreamError(e: unknown): { status?: number; code?: string; message?: string } | null {\n  if (!(e instanceof Error)) return null;\n  try { return JSON.parse(e.message); } catch { return null; } // message is the raw body text\n}","tryCatchPattern":"try {\n  return await client.chat.completions.create(body);\n} catch (e) {\n  const upstream = parseUpstreamError(e);\n  if (upstream?.error?.code === \"rate_limit_exceeded\" || upstream?.error?.type === \"rate_limit_error\") {\n    return retryWithBackoff(() => client.chat.completions.create(body), 3);\n  }\n  throw e;\n}","preventionTips":["Construct provider clients once with the upstream key: cave.openai({ upstreamKey })","Wrap every provider call in one shared error adapter that JSON.parses the message","Retry only rate-limit/transient codes; fail fast on auth and model-policy errors"],"tags":["http","provider","upstream","error-handling"],"backgroundTag":"upstream-provider-error","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}