ruvnet/ruflo · error · HttpFetchValidationError

INVALID_HEADERS

INVALID_HEADERS

Error message

headers must be an object

What it means

Before per-header validation, the http_fetch handler requires input.headers to be a plain object: typeof 'object', non-null, and not an array. Anything else — a string like 'Content-Type: application/json', an array of [name, value] pairs, or a number — throws HttpFetchValidationError 'headers must be an object' (code INVALID_HEADERS), returned as success:false with status 0. headers is optional; omitting it or passing {} is fine because the handler defaults null/undefined to {}.

Solutions

  1. Pass headers as a flat string-to-string object, e.g. { 'content-type': 'application/json' }, or omit the parameter entirely
  2. Convert tuple arrays first: Object.fromEntries([['accept','json'], ['x-trace','abc']])
  3. Parse serialized configs before the call: JSON.parse(headerJson)
  4. Log the runtime type at the call site (console.dir(input.headers)) to confirm what you are actually sending

Example fix

// before
await mcp.callTool('http_fetch', {
  url: 'https://api.example.com',
  headers: 'Content-Type: application/json', // INVALID_HEADERS: string, not object
});

// after
await mcp.callTool('http_fetch', {
  url: 'https://api.example.com',
  headers: { 'Content-Type': 'application/json' },
});
Defensive patterns

Strategy: type-guard

Validate before calling

function normalizeHeaders(input: unknown): Record<string, string> | undefined {
  if (input == null) return undefined;
  if (typeof input !== 'object' || Array.isArray(input)) {
    throw new TypeError('headers must be a flat object of string -> string');
  }
  return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, String(v)]));
}
// const headers = normalizeHeaders(args.headers);

Type guard

function isPlainHeadersObject(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}
// if (args.headers !== undefined && !isPlainHeadersObject(args.headers)) fail fast with your own message

Try / catch

try {
  const res = await httpFetch({ url, headers: raw });
  if (!res.success && res.errorCode === 'INVALID_HEADERS') {
    // log the runtime type of raw (typeof + Array.isArray) and fix the caller's shape
  }
} catch (e) {
  if (e instanceof HttpFetchValidationError && e.code === 'INVALID_HEADERS') { /* shape error: never retry as-is */ }
}

Prevention

When it happens

Trigger: Calling http_fetch with headers as a raw string (copy-pasted curl -H syntax), an array of tuples like [['accept','json']], or a JSON.stringify'd object that was never parsed. Only non-object values that are present trip the check, since `input.headers ?? {}` absorbs null/undefined.

Common situations: Translating curl commands into tool arguments; passing headers: JSON.stringify(obj) by mistake; config systems that deliver arrays of {name, value} records; LLM-generated arguments using the wrong shape.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/6892141e45171e1a. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts:199

      status: 0,
      statusText: '',
      headers: {},
      body: '',
      bodyTruncated: false,
      bytesRead: 0,
      durationMs: Date.now() - startedAt,
      url,
      method,
      error: err.message,
      errorCode: err.code ?? 'VALIDATION_ERROR',
    };
  }

  let headers: Record<string, string>;
  try {
    const raw = (input.headers ?? {}) as Record<string, string>;
    if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
      throw new HttpFetchValidationError('headers must be an object', 'INVALID_HEADERS');
    }
    headers = validateHeaders(raw);
  } catch (e) {
    const err = e as HttpFetchValidationError;
    return {
      success: false,
      status: 0,
      statusText: '',
      headers: {},
      body: '',
      bodyTruncated: false,
      bytesRead: 0,
      durationMs: Date.now() - startedAt,
      url,
      method,
      error: err.message,
      errorCode: err.code ?? 'VALIDATION_ERROR',
    };

View on GitHub (pinned to fa13ee4ad6)