JuliusBrussee/caveman · error · Error

is not an array (got )

Error message

${field} is not an array (got ${typeof value})

What it means

expectArray() normalizes optional list fields from captured harness request bodies (e.g. body.tools) before analysis. The library treats missing/null as an empty array, but any value present in a non-array shape is thrown instead of silently coerced, because a mis-shaped field counted as zero would produce a false measurement (e.g. "0 tools"). The message names the field and the actual typeof value seen.

Solutions

  1. Inspect the named field in the request body and correct it to be an array (wrap a single object in [ ... ] if one tool was supplied).
  2. If the field is truly optional/absent, delete the key or set it to null — expectArray maps both to [].
  3. Validate captured bodies before analysis with Array.isArray(body.tools) and fail fast at capture time.
  4. If a provider/harness legitimately changed shape (object map), map values with Object.values() before passing to the analyzer.

Example fix

// before
anthropicParts({ tools: { web_search: { type: "web_search" } } })
// after
const raw = { tools: { web_search: { type: "web_search" } } };
anthropicParts({ tools: Array.isArray(raw.tools) ? raw.tools : Object.values(raw.tools) })
Defensive patterns

Strategy: validation

Validate before calling

function assertArrayField(body, field) {
  const v = body?.[field];
  if (v === undefined || v === null) return [];
  if (!Array.isArray(v)) throw new TypeError(`${field} must be an array, got ${typeof v}`);
  return v;
}
// call before analysis:
assertArrayField(requestBody, "tools");

Type guard

const isArrayField = (v) => v === undefined || v === null || Array.isArray(v);

Try / catch

try {
  analyzeBody(body);
} catch (e) {
  if (/is not an array \(got /.test(e.message)) {
    console.error(`Malformed captured body: ${e.message}`); // skip or fix fixture
  } else throw e;
}

Prevention

When it happens

Trigger: Calling expectArray(value, field) — directly or via anthropicParts() — where value is defined and non-null but not an array: e.g. body.tools is an object map, a string, or a number from a malformed or hand-edited captured request body.

Common situations: Replaying a captured Claude Code / anthropic-messages request whose body was altered by a harness or proxy; hand-editing a fixture and replacing the tools array with a single tool object; a schema change in an upstream harness that emits tools as a keyed object instead of a list.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

Thrown at packages/subagent-tax/lib/analyze.mjs:20

// prints. All sizes are JSON-serialized character counts of what the harness
// actually sent — nothing is inferred here except the chars themselves.

const LLM_KINDS = new Set([
  "anthropic-messages",
  "openai-chat",
  "openai-responses",
  "gemini-generatecontent",
]);

const jsonChars = (value) => (value === undefined || value === null ? 0 : JSON.stringify(value).length);

const isSystemRole = (m) => m && (m.role === "system" || m.role === "developer");

// A field present in an unexpected shape is reported as unparsed, never
// silently counted as zero — a false "0 tools" reads as a real measurement.
function expectArray(value, field) {
  if (value === undefined || value === null) return [];
  if (!Array.isArray(value)) throw new Error(`${field} is not an array (got ${typeof value})`);
  return value;
}

function anthropicParts(body) {
  const tools = expectArray(body.tools, "tools").map((t) => ({ name: t?.name ?? t?.type ?? "?", chars: jsonChars(t) }));
  // The fixed instruction payload arrives as `system` AND, in some harnesses,
  // as system-role entries inside `messages` — count both or the flagship row
  // under-reports its own prefix.
  const messages = expectArray(body.messages, "messages");
  const sysMessages = messages.filter(isSystemRole);
  const rest = messages.filter((m) => !isSystemRole(m));
  return {
    system_chars: jsonChars(body.system) + (sysMessages.length ? jsonChars(sysMessages) : 0),
    tools,
    messages_chars: jsonChars(rest.length ? rest : undefined),
  };
}

View on GitHub (pinned to 3ee70a1026)