Skip to content

TransportHeaders

Immutable, case-insensitive, multi-value HTTP response headers. TransportHeaders is attached to UnexpectedStatusError and available on every transport response — you never need to construct one yourself.

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 behaviour.

Full API reference

pyhaul.transport._headers.TransportHeaders(items: Pairs = ())

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.

Construct via build, from_pairs, or from_mapping.

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.

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.