{"record":{"id":"74b9fc4d9b8d210a","repo":"ruvnet/ruflo","slug":"invalid-header-value","errorCode":"INVALID_HEADER_VALUE","errorMessage":"header \"${key}\" must be a string","messagePattern":"header \"(.+?)\" must be a string","errorType":"validation","errorClass":"HttpFetchValidationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts","lineNumber":116,"sourceCode":"  const out: Record<string, string> = {};\n  for (const [key, value] of Object.entries(headers)) {\n    const lower = key.toLowerCase();\n    if (!allowAuth) {\n      if ((FORBIDDEN_HEADERS_EXACT as readonly string[]).includes(lower)) {\n        throw new HttpFetchValidationError(\n          `header \"${key}\" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,\n          'FORBIDDEN_HEADER',\n        );\n      }\n      if (FORBIDDEN_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {\n        throw new HttpFetchValidationError(\n          `header \"${key}\" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,\n          'FORBIDDEN_HEADER',\n        );\n      }\n    }\n    if (typeof value !== 'string') {\n      throw new HttpFetchValidationError(\n        `header \"${key}\" must be a string`,\n        'INVALID_HEADER_VALUE',\n      );\n    }\n    out[key] = value;\n  }\n  return out;\n}\n\nfunction clampNumber(raw: unknown, defaultValue: number, max: number): number {\n  if (raw === undefined || raw === null) return defaultValue;\n  const n = Number(raw);\n  if (!Number.isFinite(n) || n <= 0) return defaultValue;\n  return Math.min(Math.floor(n), max);\n}\n\nexport interface HttpFetchResult {\n  success: boolean;","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait mcp.callTool('http_fetch', {\n  url: 'https://api.example.com',\n  headers: { 'content-type': 'application/json', 'retry-after': 3 }, // INVALID_HEADER_VALUE\n});\n\n// after\nawait mcp.callTool('http_fetch', {\n  url: 'https://api.example.com',\n  headers: { 'content-type': 'application/json', 'retry-after': String(3) },\n});","handlingStrategy":"type-guard","validationCode":"function stringifyHeaders(headers: Record<string, unknown>): Record<string, string> {\n  return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : String(v)]));\n}\n// const headers = stringifyHeaders(rawHeaders);","typeGuard":"function isStringRecord(v: unknown): v is Record<string, string> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v)\n    && Object.values(v).every(x => typeof x === 'string');\n}\n// if (!isStringRecord(headers)) headers = stringifyHeaders(headers);","tryCatchPattern":"try {\n  const res = await httpFetch({ url, headers });\n  if (!res.success && res.errorCode === 'INVALID_HEADER_VALUE') {\n    // res.error names the offending header — fix that entry, do not retry unchanged\n  }\n} catch (e) {\n  if (e instanceof HttpFetchValidationError && e.code === 'INVALID_HEADER_VALUE') { /* coerce and re-submit once */ }\n}","preventionTips":["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"],"tags":["http","mcp","headers","type-validation","json"],"backgroundTag":"header-type-mismatch","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}