JuliusBrussee/caveman · error · CaveRequestError

cave artifact was not valid JSON

Error message

cave artifact was not valid JSON

What it means

A CaveRequestError thrown by artifactGet() when the gateway responded 2xx but JSON.parse(response.text()) failed — the artifact body is not valid JSON. The status and path are attached so the failure is attributable even though the server claimed success.

Source

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

  }
  if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) {
    throw new CaveRequestError(response.status, path, "cave response must be a JSON object");
  }
  return decoded as Record<string, unknown>;
}

async function artifactGet(cave: Cave, artifactId: string, workflow: string, trace: TraceContext): Promise<unknown> {
  if (typeof artifactId !== "string" || artifactId.trim() === "") throw new Error("artifactId is required");
  const path = `/sdk/v1/artifacts/${encodeURIComponent(artifactId)}`;
  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, {
    method: "GET",
    headers: headers(cave, workflow, undefined, undefined, trace)
  });
  if (!response.ok) throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);
  try {
    return JSON.parse(await response.text());
  } catch {
    throw new CaveRequestError(response.status, path, "cave artifact was not valid JSON");
  }
}

function headers(cave: Cave, workflow: string, upstreamKey?: string, hint?: Record<string, unknown>, trace?: TraceContext) {
  return {
    "content-type": "application/json",
    authorization: `Bearer ${cave.options.apiKey}`,
    "x-cave-agent": cave.options.agent,
    "x-cave-workflow": workflow,
    "x-cave-retention": cave.options.retention ?? "metadata",
    ...(cave.options.user ? { "x-cave-user-hash": cave.options.user } : {}),
    ...(upstreamKey ? { "x-cave-upstream-key": upstreamKey } : {}),
    ...(hint?.["latencyClass"] !== undefined ? { "x-cave-async": String(hint["latencyClass"] !== "interactive") } : {}),
    ...(typeof hint?.["toolSessionId"] === "string" ? { "x-cave-tool-session": hint["toolSessionId"] as string } : {}),
    ...(typeof hint?.["assemblyHeader"] === "string" ? { "x-cave-assembly": hint["assemblyHeader"] as string } : {}),
    // Trace continuity: only requests made through a CaveTrace carry these, so
    // the gateway row's span parents onto the SDK's root span.
    ...(trace ? { "x-cave-trace-id": trace.traceId, "x-cave-parent-span-id": trace.parentSpanId } : {})

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Retry once — truncated or mangled bodies are usually transient
  2. Inspect the raw response by curling the same URL with the same headers to see what the body actually is
  3. If a proxy is involved, bypass it or add the gateway host to its exception list
  4. If reproducible against the real gateway, capture the body and report it as a server bug
Defensive patterns

Strategy: retry

Try / catch

let last;
for (let attempt = 0; attempt < 2; attempt++) {
  try { return await trace.artifacts.get(id); }
  catch (e) {
    last = e;
    if (!(e instanceof CaveRequestError) || !/not valid JSON/.test(e.message)) throw e;
  }
}
throw last; // one retry only — mangled bodies are usually transient

Prevention

When it happens

Trigger: A corporate proxy or captive portal injecting an HTML page with status 200; truncated response body from a dropped connection; content-encoding corruption; a gateway bug returning an empty body for an artifact.

Common situations: MITM proxies rewriting responses; flaky mobile or hotel networks truncating bodies; gateway version skew where the artifact route returns non-JSON on partial storage failure.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/b515fb36e7757e58. Report an issue: GitHub.