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
- 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
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
- 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
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
- cave request failed (${response.status})
- caveman build: invalid .caveman/provider.json
- caveman agent: no supported provider credential found
- --provider requires a value
- unsupported provider ${JSON.stringify(value)}
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/27aacbcde1969db9.
Report an issue: GitHub.