JuliusBrussee/caveman · error

await response.text()

Error message

await response.text()

What it means

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.

Source

Thrown at packages/sdk/typescript/src/index.ts:2039

  if (target.origin !== base.origin || (target.pathname !== prefix && !target.pathname.startsWith(`${prefix}/`))) {
    throw new Error("cave_provider_raw_path_not_allowed");
  }
  const merged = new Headers(request.headers);
  const gatewayHeaders = headers(cave, cave.options.defaultWorkflow ?? "unlabeled-workflow", upstreamKey);
  for (const [name, value] of Object.entries(gatewayHeaders)) {
    if (name !== "content-type") merged.set(name, value);
  }
  return caveFetch(cave, new Request(request, { headers: merged }));
}

async function providerFetch(cave: Cave, path: string, body: unknown, workflow: string, hint?: Record<string, unknown>, upstreamKey?: string, trace?: TraceContext) {
  const assemblyHeader = body !== null && typeof body === "object" ? assemblyRequestHeaders.get(body as object) : undefined;
  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, {
    method: "POST",
    headers: headers(cave, workflow, upstreamKey, assemblyHeader ? { ...hint, assemblyHeader } : hint, trace),
    body: JSON.stringify(body)
  });
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}

export class CaveRequestError extends Error {
  constructor(readonly status: number, readonly path: string, message: string) {
    super(message);
    this.name = "CaveRequestError";
  }
}

async function request(cave: Cave, path: string, body?: unknown, workflow?: string, trace?: TraceContext, extraHeaders?: Record<string, string>): Promise<Record<string, unknown>> {
  const init: RequestInit = {
    method: body === undefined ? "GET" : "POST",
    headers: { ...headers(cave, workflow ?? cave.options.defaultWorkflow ?? "unlabeled-workflow", undefined, undefined, trace), ...extraHeaders }
  };
  if (body !== undefined) init.body = JSON.stringify(body);
  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, init);
  if (!response.ok) throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);

View on GitHub (pinned to 766dce6b13)

Solutions

  1. JSON.parse the error message in a catch block to read the upstream error.code/message (see tryCatchPattern)
  2. Verify you constructed the client with the upstream key: cave.openai({ upstreamKey }) — the Cave apiKey alone is not the provider credential
  3. For 429, retry with exponential backoff; for 400/403 model errors, check the workflow's model policy
  4. Reproduce with the same body via .raw() to see full response headers

Example fix

// before
const completion = await cave.openai({ upstreamKey }).chat.completions.create(body); // rejects with raw body text

// after
try {
  const completion = await cave.openai({ upstreamKey }).chat.completions.create(body);
} catch (e) {
  let upstream: { error?: { code?: string; message?: string } } = {};
  try { upstream = JSON.parse((e as Error).message); } catch { /* non-JSON body */ }
  console.error("provider error:", upstream.error?.code, upstream.error?.message);
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

function assertProviderClientConfigured(cave: Cave, upstreamKey: string | undefined): void {
  if (!upstreamKey) throw new Error("upstreamKey missing — provider calls will fail at the gateway");
}

Type guard

function parseUpstreamError(e: unknown): { status?: number; code?: string; message?: string } | null {
  if (!(e instanceof Error)) return null;
  try { return JSON.parse(e.message); } catch { return null; } // message is the raw body text
}

Try / catch

try {
  return await client.chat.completions.create(body);
} catch (e) {
  const upstream = parseUpstreamError(e);
  if (upstream?.error?.code === "rate_limit_exceeded" || upstream?.error?.type === "rate_limit_error") {
    return retryWithBackoff(() => client.chat.completions.create(body), 3);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/27aacbcde1969db9. Report an issue: GitHub.