Skip to content

Cookies, redirects, proxy & TLS

async with HTTPClient.build(
    base_url="https://api.example.com/",
    cookie_store=True,  # in-memory cookie jar, sent automatically on subsequent requests
    follow_redirects=True,  # default
    max_redirects=10,  # None (default) leaves pyreqwest's own limit in place
    proxy="http://localhost:8080",
) as client:
    ...

Unlike timeout and auth (see Authentication), none of these three take a per-call override — pyreqwest's RequestBuilder has no .follow_redirects()/.max_redirects()/.proxy()/ .default_cookie_store() at all, only its ClientBuilder does. That's not a gap lothc could close: redirect policy, proxy, and the cookie jar are all client/connection-level constructs in the underlying Rust reqwest crate itself, not per-request ones — the proxy decides which TCP connection gets opened, the cookie jar is shared state, and the redirect policy governs the transport loop before a response ever reaches request-level code. If one call genuinely needs different redirect/proxy/cookie behavior than the rest, build a second client with that setting rather than looking for a per-verb kwarg.

TLS & mTLS

async with HTTPClient.build(
    base_url="https://internal-api.example.com/",
    root_certificates=[Path("internal-ca.pem").read_bytes()],  # trust a custom/internal CA
    identity_pem=Path("client-identity.pem").read_bytes(),  # mTLS: cert + private key, one PEM
    min_tls_version="TLSv1.2",
    max_tls_version="TLSv1.3",
    https_only=True,  # refuse plain HTTP entirely
) as client:
    ...

root_certificates takes any number of PEM-encoded certificates — each one is trusted in addition to (not instead of) the system's own root store. identity_pem is a single PEM buffer containing both the client certificate and its private key concatenated, exactly as reqwest's own Identity::from_pem expects.

There's also danger_accept_invalid_certs: bool = False, which disables certificate validation entirely — insecure, and only ever appropriate against a local/test endpoint you control, never in production.

Like timeout, these are all client-level only — set once at build(), no per-call override, since a TLS/connection identity belongs to the underlying connection, not a single request.

Connection pooling

async with HTTPClient.build(
    base_url="https://api.example.com/",
    connect_timeout=5.0,  # bounds only the TCP connect phase, separate from `timeout`
    read_timeout=60.0,  # max idle gap between body chunks — the stall detector for `sse()`
    max_connections=50,
    pool_idle_timeout=30.0,  # None (default) leaves pyreqwest's own 90s default in place
    pool_max_idle_per_host=10,
    pool_timeout=2.0,  # max wait for a free connection slot
) as client:
    ...

connect_timeout is distinct from timeout — it only bounds the initial TCP connect, not the whole request, so it's useful for "fail fast on a dead host" without capping how long a slow (but alive) download is allowed to take.

read_timeout is distinct from both — it bounds the idle gap between two consecutive body chunks, not the whole request. A body that keeps trickling never trips it; one that stalls does. That makes it the one timeout that's actually meaningful for an open-ended sse() stream, which the total timeout deliberately doesn't apply to — see SSE → Timeouts.