Skip to content

TransportHeaders

Immutable, case-insensitive, multi-value HTTP headers used throughout the transport layer. Most callers encounter them as response metadata: UnexpectedStatusError carries them, and every TransportResponse exposes normalized response headers.

On the request path, pyhaul also represents the merged outbound header set as TransportHeaders: user headers plus structural defaults, then the result of prepare_headers() before your adapter's HTTP client sees them. You rarely construct request-side instances yourself; haul() / haul_async() accept an optional plain mapping for extras.

Quick usage

from pyhaul import UnexpectedStatusError

try:
    result = haul(url, client, dest="file.bin")
except UnexpectedStatusError as exc:
    h = exc.headers

    h["Content-Type"]              # first value or KeyError
    h.get("Retry-After")           # first value or None
    h.get("Retry-After", "60")     # first value or default
    "etag" in h                    # case-insensitive membership
    len(h)                         # number of unique header names

    h.get_all("Set-Cookie")        # all values in order → tuple[str, ...]

    merged = h | {"X-Extra": "v"}  # merge → new TransportHeaders
    new = h.replace("Accept", "application/json")  # functional update

Because headers are immutable and hashable, they are safe to attach to exceptions, cache, or pass across threads.

Sensitive header redaction

repr() and to_safe_dict() automatically replace values for Authorization, Proxy-Authorization, Cookie, Set-Cookie, and X-API-Key with a fixed-length [redacted] placeholder:

from pyhaul.transport._headers import TransportHeaders

h = TransportHeaders.from_pairs([
    ("Content-Type", "text/html"),
    ("Authorization", "Bearer sk-secret-token"),
])

repr(h)
# "TransportHeaders([('content-type', 'text/html'), ('authorization', '[redacted]')])"

h.to_safe_dict()
# {'content-type': 'text/html', 'authorization': '[redacted]'}

This removes a common footgun when headers end up in logs, tracebacks, or error messages.

Adapter fidelity

Multi-value and ordering fidelity varies by HTTP client:

Client Multi-value headers Order
httpx All preserved Wire order
aiohttp All preserved Wire order
requests All preserved (via resp.raw) Grouped by name
niquests All preserved (via resp.raw) Grouped by name
urllib3 All preserved Grouped by name

See HTTP Client Adapters for details on each adapter's behavior.

Full API reference

pyhaul.transport._headers.TransportHeaders(items: Iterable[Pair] = ())

Bases: Mapping[str, str]

Immutable, case-insensitive HTTP headers with multi-value support.

Field names are matched case-insensitively per HTTP semantics. Duplicate names are preserved in wire order.

Constructor arguments are normalized (names lowercased/stripped, values stripped) — same rules as :meth:from_pairs. Prefer :meth:build, :meth:from_pairs, or :meth:from_mapping for readability.

getlist = get_all class-attribute instance-attribute

Alias for get_all (werkzeug / multidict naming).

raw_items: Pairs property

All (lowercase_name, value) pairs in wire order.

build(source: Mapping[str, str] | Iterable[Pair] | None = None, /, **extra: str) -> Self classmethod

Build from a mapping or pair-iterable, with optional kwargs.

Underscores in keyword names are translated to dashes::

TransportHeaders.build(
    {"Content-Type": "text/html"},
    x_request_id="abc-123",
)

from_pairs(pairs: Iterable[Pair]) -> Self classmethod

Build from an ordered (name, value) iterable.

Preserves duplicate names and wire order for get_all. Normalization matches direct :class:TransportHeaders construction.

from_mapping(mapping: Mapping[str, str]) -> Self classmethod

Build from a single-value-per-key mapping.

Keys that differ only by case become separate entries.

get_all(name: str) -> tuple[str, ...]

Return every value for name in wire order; () if absent.

multi_items() -> Iterator[Pair]

Yield every (name, value) pair, including duplicates.

get(key: str, default: T | None = None) -> str | T | None

get(key: str) -> str | None
get(key: str, default: str) -> str
get(key: str, default: T) -> str | T

Return the first value for key, or default if absent.

__bool__() -> bool

Empty headers are falsy.

__or__(other: object) -> TransportHeaders

headers | other returns new headers with other appended.

__ror__(other: object) -> TransportHeaders

other | headers returns new headers with other prepended.

with_added(name: str, value: str) -> Self

Append (name, value) and return a new instance.

without(name: str) -> Self

Drop all entries for name and return a new instance.

replace(name: str, value: str) -> Self

Drop all entries for name then append a single new one.

to_safe_dict() -> dict[str, str]

Header dict with sensitive values redacted.

Suitable for passing to structured loggers like structlog::

logger.info("response", headers=headers.to_safe_dict())

to_wire() -> bytes

Serialize to b'name: value\r\n...' form.

Uses UTF-8 for the combined header lines. HTTP/1 historically treated field values as ISO-8859-1 octets; UTF-8 is common for Unicode today, and matches how aiohttp serializes outgoing prelude lines and how httpx defaults header encoding when emitting bytes from str values.