JuliusBrussee/caveman · warning

missing response decoder

Error message

missing response decoder

What it means

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.

Solutions

  1. Make your mock/custom Response implement text() (and/or json()) returning a JSON string or parsed object
  2. Prefer realistic stubs (e.g. undici MockAgent, nock with proper Response) over hand-rolled objects
  3. Check the fetch polyfill version and upgrade to one with full Response API support
  4. Catch the resulting CaveRequestError with message 'cave response was not valid JSON' and inspect the injected fetch

Example fix

// before
fetchMock.mockResolvedValue({ ok: true, status: 200 });
// after
fetchMock.mockResolvedValue({ ok: true, status: 200, text: async () => JSON.stringify({ ok: true }), json: async () => ({ ok: true }) });
Defensive patterns

Strategy: validation

Validate before calling

function isValidFetch(f: unknown): boolean { return typeof f === 'function'; }
// and for the response: typeof (res as Response).text === 'function' || typeof (res as Response).json === 'function'

Type guard

function hasDecoder(res: unknown): res is Response { return !!res && typeof (res as Response).text === 'function' && typeof (res as Response).json === 'function'; }

Try / catch

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; }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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

Appendix: source

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

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) {
    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) {

View on GitHub (pinned to 3ee70a1026)