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