{"record":{"id":"44e697f599351214","repo":"JuliusBrussee/caveman","slug":"cave-response-must-be-a-json-object","errorCode":null,"errorMessage":"cave response must be a JSON object","messagePattern":"cave response must be a JSON object","errorType":"validation","errorClass":"CaveRequestError","httpStatus":null,"severity":"error","filePath":"packages/sdk/typescript/src/index.ts","lineNumber":2138,"sourceCode":"  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, init);\n  if (!response.ok) {\n    discardResponse(response);\n    throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);\n  }\n  let decoded: unknown;\n  try {\n    if (typeof response.text === \"function\") {\n      decoded = JSON.parse(await response.text());\n    } else if (typeof response.json === \"function\") {\n      decoded = await response.json();\n    } else {\n      throw new Error(\"missing response decoder\");\n    }\n  } catch {\n    throw new CaveRequestError(response.status, path, \"cave response was not valid JSON\");\n  }\n  if (decoded === null || typeof decoded !== \"object\" || Array.isArray(decoded)) {\n    throw new CaveRequestError(response.status, path, \"cave response must be a JSON object\");\n  }\n  return decoded as Record<string, unknown>;\n}\n\nasync function artifactGet(cave: Cave, artifactId: string, workflow: string, trace: TraceContext): Promise<unknown> {\n  if (typeof artifactId !== \"string\" || artifactId.trim() === \"\") throw new Error(\"artifactId is required\");\n  const path = `/sdk/v1/artifacts/${encodeURIComponent(artifactId)}`;\n  const response = await caveFetch(cave, `${cave.options.baseURL}${path}`, {\n    method: \"GET\",\n    headers: headers(cave, workflow, undefined, undefined, trace)\n  });\n  if (!response.ok) {\n    discardResponse(response);\n    throw new CaveRequestError(response.status, path, `cave request failed (${response.status})`);\n  }\n  try {\n    return JSON.parse(await response.text());\n  } catch {","sourceCodeStart":2120,"sourceCodeEnd":2156,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/sdk/typescript/src/index.ts#L2120-L2156","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log err.status and err.path, then fetch the endpoint manually to see the actual JSON shape","Upgrade or align SDK and Cave server versions so the response envelope matches what the SDK expects","If you run a mock Cave, return object-wrapped payloads (e.g. { items: [...] }, { report: ... }) not bare arrays/scalars","Wrap calls in try/catch on CaveRequestError and fall back to raw HTTP inspection for debugging"],"exampleFix":"// before\n// mock server returns: [{ id: 'a1' }]\n// after\n// mock server returns: { items: [{ id: 'a1' }] }","handlingStrategy":"try-catch","validationCode":"// pre-flight shape probe:\nconst res = await fetch(baseURL + '/sdk/v1/health');\nconst body = await res.json();\nif (body === null || typeof body !== 'object' || Array.isArray(body)) throw new Error('endpoint not returning JSON objects');","typeGuard":"const isJsonObject = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);","tryCatchPattern":"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; }","preventionTips":["Pin SDK and Cave server versions together and upgrade in lockstep","Add a contract test asserting each endpoint's response is a JSON object","Ensure mocks return object-wrapped payloads, never bare arrays/scalars","Document response envelopes when changing server endpoints"],"tags":["json","schema","api","version-mismatch"],"backgroundTag":"unexpected-response-shape","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}