Skip to content

yggdrasil.http_.headers

headers

HTTP header container — :class:HTTPHeaders.

A case-insensitive (RFC 7230 §3.2) string→string mapping that preserves each field's original casing for faithful serialization, bumps a version counter on mutation for cache fingerprinting, and carries the canonical-name normalization + anonymization vocabulary. This is the single header type the request / response / session pipeline speaks.

PromotedHeaders dataclass

PromotedHeaders(
    host: str | None = None,
    user_agent: str | None = None,
    accept: str | None = None,
    accept_encoding: str | None = None,
    accept_language: str | None = None,
    content_type: str | None = None,
    content_length: int = 0,
    content_encoding: str | None = None,
    transfer_encoding: str | None = None,
    remaining: dict[str, str] = dict(),
)

Common HTTP headers extracted into dedicated typed fields.

remaining contains all non-promoted headers after normalization.

extract classmethod

extract(
    headers: Mapping[HeaderValue, HeaderValue],
    *,
    normalize: bool = True,
    host: str | None = None
) -> "PromotedHeaders"

Extract common headers into typed attributes.

Matching is case-insensitive. When normalize=True, recognized header names are canonicalized in remaining.

HTTPHeaders

HTTPHeaders(
    data: "HTTPHeaders | Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None" = None,
)

Bases: MutableMapping[str, str]

Mutable string-string mapping with versioned change tracking.

Acts as a drop-in replacement for the plain dict[str, str] that :class:Session, :class:PreparedRequest, and :class:Response were using. Two reasons to specialize it:

  • Version counter. Every mutation that actually changes the contents bumps :attr:version. (id(headers), version) is a tight cache fingerprint — id covers wholesale dict swaps, version covers in-place mutations the id can't see (the common case: headers["Authorization"] = new_token on the same dict object).
  • Cached digests. :attr:byte_length and :attr:xxh3_64 memoize against the version, so a request that hashes the same headers ten times pays the walk once. Same-value writes (headers["Accept"] = "*/*" when it already is) short-circuit without bumping the version, so re-applying defaults is free.

Keys and values are coerced to str on the way in. The store keeps each name's original case (for faithful serialization), but lookups are case-insensitive per RFC 7230 §3.2 — headers.get("Location") finds a wire location (HTTP/2 lowercases every field name) and a redirect / Retry-After read works regardless of how the origin cased it. Canonical Title-Case rewriting still lives in :func:normalize_headers, separate from this container.

version property

version: int

Monotonically increasing counter — bumped on every mutation that actually changes :meth:__eq__. Callers can stash (id(headers), version) and re-check it later for an O(1) "did anything change?" test.

byte_length property

byte_length: int

Total byte length of all keys + values. Memoized — pays the walk once per :attr:version and serves an int the rest of the time.

canonical_bytes property

canonical_bytes: bytes

Sorted key=value\x00key=value\x00… byte sequence — the canonical wire form used by digest mixing. Order is deterministic (sorted by key) so HTTPHeaders({A:1, B:2}) and HTTPHeaders({B:2, A:1}) produce the same payload. Memoized against :attr:version so each request pays the encode walk once across :attr:xxh3_64, :attr:PreparedRequest.hash, and :attr:Response.hash.

xxh3_64 property

xxh3_64: int

xxh3_64 digest over :attr:canonical_bytes — order- independent (same pairs → same digest). Returns a signed int64 to match the rest of the codebase's :func:xxhash use. Memoized against :attr:version.

from_ classmethod

from_(
    arg: "HTTPHeaders | Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None" = None,
) -> "HTTPHeaders"

Coerce arg to :class:HTTPHeaders — passing an existing instance through unchanged so callers can HTTPHeaders.from_(x) regardless of what x is.

update

update(*args: Any, **kwargs: Any) -> None

Bulk update — same shape as :meth:dict.update.

We override the mixin so the version bumps once per actual change rather than once per touched key, and so the no-op early return matches :meth:__setitem__.

copy

copy() -> 'HTTPHeaders'

Shallow copy (version reset).

Bypasses __init__'s per-item str() coercion — we already know the source's keys / values are strings and the rest of the cache slots start fresh.

to_dict

to_dict() -> dict[str, str]

Snapshot as a plain dict — handy for code that needs to mutate independently of the live container or hand the contents to an API that only speaks dict.

canonical_name staticmethod

canonical_name(name: HeaderValue) -> str

Canonical casing for name (e.g. content-typeContent-Type). Falls back to the caller's stripped form when the header isn't in the registry.

anonymized

anonymized(mode: Literal['remove', 'redact'] = 'remove') -> 'HTTPHeaders'

Return a copy with sensitive values dropped/redacted.

Authorization scheme (Bearer, Basic, …) is preserved in redact mode; long token-shaped values are caught via :func:_looks_like_token so unrecognized credential headers still get sanitized. Names are canonicalized on the way out so repeated normalize calls are idempotent.

Memoized against :attr:version — the canonical anonymized form is hot on every request's public_hash / public_url_hash computation, and per response's public_hash. A version bump on the source headers invalidates the cache transparently.

normalized

normalized(
    *,
    is_request: bool,
    add_missing: bool = True,
    mode: Literal["remove", "redact"] = "remove",
    anonymize: bool = False,
    body: "Optional[Holder]" = None
) -> "HTTPHeaders"

Return a fresh :class:HTTPHeaders with names canonicalized, sensitive values optionally sanitized, and (when add_missing) request defaults / body-derived Content-* slots filled in.

This is the canonical normalize surface — :func:normalize_headers is a thin wrapper that calls into this method so every canonicalization site goes through one piece of code.

normalize_headers

normalize_headers(
    headers: "HTTPHeaders | Mapping[HeaderValue, HeaderValue] | None",
    *,
    is_request: bool,
    add_missing: bool = True,
    mode: Literal["remove", "redact"] = "remove",
    anonymize: bool = False,
    body: Optional[IO] = None
) -> "HTTPHeaders"

Backwards-compatible thin wrapper around :meth:HTTPHeaders.normalized.

The whole normalization vocabulary (canonical names, sensitive detection, body-derived Content-* backfill, request defaults) lives on :class:HTTPHeaders so a single audit covers the whole behaviour. This free function stays so existing callers (normalize_headers(some_dict, is_request=True)) keep working without rewriting every site.