JuliusBrussee/caveman · error · CaveRequestError

cave response must be a JSON object

Error message

cave response must be a JSON object

What it means

After the shared caveFetch helper successfully parses the response body as JSON, it validates that the decoded value is a non-null, non-array object before returning it as Record<string, unknown>. If the Cave endpoint legitimately answered 2xx with null, a top-level array, a string, or a number, this CaveRequestError is thrown to enforce the SDK's contract that every JSON-object endpoint returns an object.

Solutions

  1. Log err.status and err.path, then fetch the endpoint manually to see the actual JSON shape
  2. Upgrade or align SDK and Cave server versions so the response envelope matches what the SDK expects
  3. If you run a mock Cave, return object-wrapped payloads (e.g. { items: [...] }, { report: ... }) not bare arrays/scalars
  4. Wrap calls in try/catch on CaveRequestError and fall back to raw HTTP inspection for debugging

Example fix

// before
// mock server returns: [{ id: 'a1' }]
// after
// mock server returns: { items: [{ id: 'a1' }] }
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight shape probe:
const res = await fetch(baseURL + '/sdk/v1/health');
const body = await res.json();
if (body === null || typeof body !== 'object' || Array.isArray(body)) throw new Error('endpoint not returning JSON objects');

Type guard

const isJsonObject = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);

Try / catch

try { const r = await sdk.call(); } catch (e) { if (e instanceof CaveRequestError && e.message.includes('must be a JSON object')) log.error(`version skew: unexpected envelope at ${e.path}`); throw e; }

Prevention

When it happens

Trigger: A Cave server version that returns an array for a collection endpoint the SDK expects to be an object wrapper ({ items: [...] }), a proxy serving JSON scalars/null, or a misrouted request hitting an endpoint that returns a bare JSON literal with 200.

Common situations: Client/server version skew after a server upgrade changed the response envelope; custom or mocked Cave service returning arrays directly; hitting a health-check route that answers "ok" (JSON string) with 200.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/44e697f599351214. Report an issue: GitHub.

Appendix: source

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

  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, init);
  if (!response.ok) {
    discardResponse(response);
    throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);
  }
  let decoded: unknown;
  try {
    if (typeof response.text === "function") {
      decoded = JSON.parse(await response.text());
    } else if (typeof response.json === "function") {
      decoded = await response.json();
    } else {
      throw new Error("missing response decoder");
    }
  } catch {
    throw new CaveRequestError(response.status, path, "cave response was not valid JSON");
  }
  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) {
    discardResponse(response);
    throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);
  }
  try {
    return JSON.parse(await response.text());
  } catch {

View on GitHub (pinned to 3ee70a1026)