{"id":"c6a02126cc4ae39c","repo":"encode/httpx","slug":"header-value-must-be-str-or-bytes-not-type-value","errorCode":null,"errorMessage":"Header value must be str or bytes, not {type(value)}","messagePattern":"Header value must be str or bytes, not (.+?)","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"httpx/_models.py","lineNumber":81,"sourceCode":"        return False\n    return True\n\n\ndef _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes:\n    \"\"\"\n    Coerce str/bytes into a strictly byte-wise HTTP header key.\n    \"\"\"\n    return key if isinstance(key, bytes) else key.encode(encoding or \"ascii\")\n\n\ndef _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:\n    \"\"\"\n    Coerce str/bytes into a strictly byte-wise HTTP header value.\n    \"\"\"\n    if isinstance(value, bytes):\n        return value\n    if not isinstance(value, str):\n        raise TypeError(f\"Header value must be str or bytes, not {type(value)}\")\n    return value.encode(encoding or \"ascii\")\n\n\ndef _parse_content_type_charset(content_type: str) -> str | None:\n    # We used to use `cgi.parse_header()` here, but `cgi` became a dead battery.\n    # See: https://peps.python.org/pep-0594/#cgi\n    msg = email.message.Message()\n    msg[\"content-type\"] = content_type\n    return msg.get_content_charset(failobj=None)\n\n\ndef _parse_header_links(value: str) -> list[dict[str, str]]:\n    \"\"\"\n    Returns a list of parsed link headers, for more info see:\n    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link\n    The generic syntax of those is:\n    Link: < uri-reference >; param1=value1; param2=\"value2\"\n    So for instance:","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/encode/httpx/blob/b5addb64f0161ff6bfe94c124ef76f6a1fba5254/httpx/_models.py#L63-L99","documentation":"TypeError raised by _normalize_header_value when a header value is neither str nor bytes. httpx headers are normalized to bytes, so any other type (int, None, dict, list, custom object) is rejected. This fires during Headers construction or any header mutation that funnels through _normalize_header_value (e.g. client.headers['X-Count'] = 5).","triggerScenarios":"Setting a header to an int (X-Total: 5), float, None, bool, or list; passing a Headers object/dict whose values are non-string; client(headers={'X-Retry': 3}); response.headers manipulation with numeric counters.","commonSituations":"Dynamic header values computed from numeric data without str(); None passed where a string was expected (e.g. missing config); booleans from toggles; CSV lists meant to be joined; datetime objects not isoformatted.","solutions":["Coerce to str: client.headers['X-Count'] = str(count).","For None, use a guard: headers['X-Opt'] = value if value is not None else ''.","For lists, join: ', '.join(values).","For datetimes, .isoformat() or format explicitly."],"exampleFix":"// before\nclient.headers['X-Retry-Count'] = retries  # int -> TypeError\n// after\nclient.headers['X-Retry-Count'] = str(retries)","handlingStrategy":"type-guard","validationCode":"def coerce_header_value(value):\n    if isinstance(value, (str, bytes)):\n        return value\n    if value is None:\n        return ''\n    return str(value)\n\nheaders = {k: coerce_header_value(v) for k, v in raw_headers.items()}","typeGuard":"def is_valid_header_value(value) -> bool:\n    return isinstance(value, (str, bytes))","tryCatchPattern":"try:\n    client.headers['X-Count'] = count\nexcept TypeError:\n    client.headers['X-Count'] = str(count)","preventionTips":["Always coerce header values to str/bytes at the boundary.","Handle None explicitly (empty string or skip).","Join lists with ', ' rather than passing them raw.","Format datetimes/objects before assigning to headers."],"tags":["headers","type-checking","validation","request-body"],"analyzedSha":"b5addb64f0161ff6bfe94c124ef76f6a1fba5254","analyzedAt":"2026-08-04T19:32:56.768Z","schemaVersion":2}