ruvnet/ruflo · error · HttpFetchValidationError

INVALID_HEADER_VALUE

INVALID_HEADER_VALUE

Error message

header "${key}" must be a string

What it means

After the forbidden-name checks pass, validateHeaders requires every header value to have typeof 'string'. Any number, boolean, array, nested object, or null value throws HttpFetchValidationError with code INVALID_HEADER_VALUE, which the http_fetch handler returns as success:false, status:0, errorCode:'INVALID_HEADER_VALUE'. The MCP JSON input schema does not enforce per-value types, so this runtime check is the last line of defense before the fetch is issued.

Solutions

  1. Stringify every value before the call: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, String(v)]))
  2. For multi-value headers, join them: ['application/json', 'text/html'].join(', ')
  3. Look at the header name quoted in the message and fix that one entry in the caller config
  4. Annotate the call site with a Record<string, string> type so TypeScript flags non-string values at compile time

Example fix

// before
await mcp.callTool('http_fetch', {
  url: 'https://api.example.com',
  headers: { 'content-type': 'application/json', 'retry-after': 3 }, // INVALID_HEADER_VALUE
});

// after
await mcp.callTool('http_fetch', {
  url: 'https://api.example.com',
  headers: { 'content-type': 'application/json', 'retry-after': String(3) },
});
Defensive patterns

Strategy: type-guard

Validate before calling

function stringifyHeaders(headers: Record<string, unknown>): Record<string, string> {
  return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)]));
}
// const headers = stringifyHeaders(rawHeaders);

Type guard

function isStringRecord(v: unknown): v is Record<string, string> {
  return typeof v === 'object' && v !== null && !Array.isArray(v)
    && Object.values(v).every(x => typeof x === 'string');
}
// if (!isStringRecord(headers)) headers = stringifyHeaders(headers);

Try / catch

try {
  const res = await httpFetch({ url, headers });
  if (!res.success && res.errorCode === 'INVALID_HEADER_VALUE') {
    // res.error names the offending header — fix that entry, do not retry unchanged
  }
} catch (e) {
  if (e instanceof HttpFetchValidationError && e.code === 'INVALID_HEADER_VALUE') { /* coerce and re-submit once */ }
}

Prevention

When it happens

Trigger: headers: { 'content-type': ['application/json'] } (array), { 'retry-after': 3 } (number), { 'accept': null }, or values coming from JSON.parse of a config where numbers were never quoted. A single non-string value fails the entire call before any network I/O.

Common situations: Building headers from a typed config object (numbers/booleans) without stringifying; spreading a parsed JSON options file into headers; LLM-generated tool arguments embedding a bare number or array; migrating from a header utility that accepted arrays for multi-value headers.

Related errors


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

Appendix: source

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

  const out: Record<string, string> = {};
  for (const [key, value] of Object.entries(headers)) {
    const lower = key.toLowerCase();
    if (!allowAuth) {
      if ((FORBIDDEN_HEADERS_EXACT as readonly string[]).includes(lower)) {
        throw new HttpFetchValidationError(
          `header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,
          'FORBIDDEN_HEADER',
        );
      }
      if (FORBIDDEN_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {
        throw new HttpFetchValidationError(
          `header "${key}" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,
          'FORBIDDEN_HEADER',
        );
      }
    }
    if (typeof value !== 'string') {
      throw new HttpFetchValidationError(
        `header "${key}" must be a string`,
        'INVALID_HEADER_VALUE',
      );
    }
    out[key] = value;
  }
  return out;
}

function clampNumber(raw: unknown, defaultValue: number, max: number): number {
  if (raw === undefined || raw === null) return defaultValue;
  const n = Number(raw);
  if (!Number.isFinite(n) || n <= 0) return defaultValue;
  return Math.min(Math.floor(n), max);
}

export interface HttpFetchResult {
  success: boolean;

View on GitHub (pinned to fa13ee4ad6)