{"record":{"id":"32e5138a8fafb498","repo":"JuliusBrussee/caveman","slug":"missing-response-decoder","errorCode":null,"errorMessage":"missing response decoder","messagePattern":"missing response decoder","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/sdk/typescript/src/index.ts","lineNumber":2132,"sourceCode":"async function request(cave: Cave, path: string, body?: unknown, workflow?: string, trace?: TraceContext, extraHeaders?: Record<string, string>): Promise<Record<string, unknown>> {\n  const init: RequestInit = {\n    method: body === undefined ? \"GET\" : \"POST\",\n    headers: { ...headers(cave, workflow ?? cave.options.defaultWorkflow ?? \"unlabeled-workflow\", undefined, undefined, trace), ...extraHeaders }\n  };\n  if (body !== undefined) init.body = JSON.stringify(body);\n  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) {","sourceCodeStart":2114,"sourceCodeEnd":2150,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/sdk/typescript/src/index.ts#L2114-L2150","documentation":"An internal guard inside the shared caveFetch response decoder: if the injected fetch-like Response object exposes neither a .text() nor a .json() function, the code throws this plain Error, which is immediately caught by the surrounding try/catch and rethrown as CaveRequestError(\"cave response was not valid JSON\"). It normally indicates a non-standard fetch implementation (custom mock, polyfill, or undici shim) rather than a server problem.","triggerScenarios":"Passing a custom fetch implementation via the Cave options whose Response objects lack both text and json methods (e.g. a mock returning { ok: true } only, or a Node http callback wrapper that yields a raw stream object).","commonSituations":"Test suites stubbing global fetch with simplified objects; environments with an incomplete fetch polyfill; wrapping fetch to add metrics but returning a custom response shape.","solutions":["Make your mock/custom Response implement text() (and/or json()) returning a JSON string or parsed object","Prefer realistic stubs (e.g. undici MockAgent, nock with proper Response) over hand-rolled objects","Check the fetch polyfill version and upgrade to one with full Response API support","Catch the resulting CaveRequestError with message 'cave response was not valid JSON' and inspect the injected fetch"],"exampleFix":"// before\nfetchMock.mockResolvedValue({ ok: true, status: 200 });\n// after\nfetchMock.mockResolvedValue({ ok: true, status: 200, text: async () => JSON.stringify({ ok: true }), json: async () => ({ ok: true }) });","handlingStrategy":"validation","validationCode":"function isValidFetch(f: unknown): boolean { return typeof f === 'function'; }\n// and for the response: typeof (res as Response).text === 'function' || typeof (res as Response).json === 'function'","typeGuard":"function hasDecoder(res: unknown): res is Response { return !!res && typeof (res as Response).text === 'function' && typeof (res as Response).json === 'function'; }","tryCatchPattern":"try { const r = await sdk.call(); } catch (e) { if (e instanceof CaveRequestError && e.message.includes('not valid JSON')) console.error('check custom fetch Response shape'); throw e; }","preventionTips":["Use established mocking libs (undici MockAgent, nock) instead of hand-built Response objects","Ensure every custom fetch wrapper returns a real or fully-shaped Response","Type the custom fetch as typeof fetch so missing Response methods fail compilation","Test the full SDK call path, not just fetch, in unit tests"],"tags":["fetch","mocking","typescript","response"],"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"}