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
- Stringify every value before the call: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, String(v)]))
- For multi-value headers, join them: ['application/json', 'text/html'].join(', ')
- Look at the header name quoted in the message and fix that one entry in the caller config
- 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
- Type every header object as Record<string, string> at the boundary where it is built
- Never spread parsed JSON or typed configs into headers without a String() mapping pass
- Join multi-value headers with ', ' instead of passing arrays
- Unit-test the header builder with numbers/booleans/null to prove coercion happens
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
- INVALID_HEADERS
- FORBIDDEN_HEADER
- Failed to fetch base servers
- FORBIDDEN_PROTOCOL
- HTTP transport already running
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)