# httpxr — LLM Context File # https://github.com/bmsuisse/httpxr # https://pypi.org/project/httpxr/ # Docs: https://bmsuisse.github.io/httpxr/ ## Project Overview **httpxr** is a high-performance, 1:1 Rust port of the Python `httpx` HTTP client. - **Goal**: 100% API compatibility with `httpx` — swap `import httpx` for `import httpxr`. - **Performance**: ~2.4× faster sequentially, ~13× faster under concurrency vs httpx. - **Zero Python dependencies**: Everything (HTTP, TLS, compression, SOCKS, IDNA) is native Rust. - **Python versions**: 3.12, 3.13, 3.14 (including free-threaded builds). - **License**: MIT OR Apache-2.0. ## Technology Stack | Layer | Technology | | :----------------- | :-------------------------------------------------- | | Python bindings | PyO3 0.28 | | Async runtime | tokio (multi-threaded) | | HTTP engine | reqwest 0.13 (rustls + native-tls) | | JSON parsing | serde_json + simd-json | | Compression | gzip (flate2), brotli, zstd, deflate (all native) | | Memory allocator | mimalloc | | Build system | maturin | | Dependency mgmt | uv | | Docs | mkdocs-material | ## Architecture The project is a native Python extension module (`_httpxr`) compiled from Rust. ``` ┌─────────────────────────────────────────────────────┐ │ Python layer (httpxr/) │ │ __init__.py ← re-exports everything from _httpxr │ │ _exceptions.py ← patches HTTPError/RequestError │ │ _transports/ ← WSGITransport, ASGITransport │ │ _urlparse.py ← urlparse() compat helper │ │ _utils.py ← Python-side utilities │ │ cli.py ← CLI (optional, click-based) │ │ compat.py ← httpx shim (import httpx → httpxr) │ │ _httpxr.pyi ← full type stubs for IDE support │ └────────┬────────────────────────────────────────────┘ │ PyO3 FFI ┌────────▼────────────────────────────────────────────┐ │ Rust extension (_httpxr) │ │ lib.rs ← module registration, mimalloc │ │ client/ ← Client & AsyncClient impls │ │ models/ ← Request, Response, Headers, etc │ │ transports/ ← HTTP transport layer (reqwest) │ │ api.rs ← top-level get/post/put/… funcs │ │ auth.rs ← BasicAuth, DigestAuth │ │ config.rs ← Timeout, Limits, Proxy, Retry │ │ content.rs ← request body encoding │ │ decoders.rs ← gzip/brotli/zstd decompression │ │ exceptions.rs ← exception hierarchy │ │ multipart.rs ← multipart form uploads │ │ query_params.rs ← QueryParams type │ │ status_codes.rs ← HTTP status codes enum │ │ urls.rs ← URL type (30k lines) │ │ urlparse.rs ← URL parsing utilities │ │ types.rs ← shared type helpers │ │ utils.rs ← internal utility functions │ │ logger.rs ← Python logging bridge │ │ stream_ctx.rs ← streaming context manager │ └─────────────────────────────────────────────────────┘ ``` ### Rust Source Layout (src/) ``` src/ ├── lib.rs # Entry point — registers all submodules, sets mimalloc allocator ├── api.rs # Top-level functions: get(), post(), put(), delete(), etc. ├── auth.rs # BasicAuth, DigestAuth (MD5/SHA-256, algorithm negotiation) ├── config.rs # Timeout, Limits, Proxy, RetryConfig, create_ssl_context() ├── content.rs # Request body encoding (bytes, json, form data) ├── decoders.rs # Response decompression: gzip, brotli, zstd, deflate ├── exceptions.rs # Full httpx exception hierarchy (HTTPError → … → ReadTimeout) ├── logger.rs # Bridge Rust log → Python logging module ├── multipart.rs # Multipart form data encoder (DataField, FileField, MultipartStream) ├── query_params.rs # QueryParams (immutable multidict) ├── status_codes.rs # HTTP status codes (IntEnum-like: codes.OK, codes.NOT_FOUND, …) ├── stream_ctx.rs # Streaming response context manager ├── types.rs # Type conversion helpers (to_bytes, to_str, …) ├── urlparse.rs # URL parsing & encoding (IDNA, percent-encoding) ├── urls.rs # URL class with full component access ├── utils.rs # Internal utilities ├── client/ │ ├── mod.rs # Module exports │ ├── common.rs # Shared client logic (merging headers, auth, redirects, event hooks) │ ├── sync_client.rs # Client — synchronous HTTP client │ ├── sync_client_methods.rs # Client HTTP method macros (get, post, put, …) │ ├── sync_client_send.rs # Client.send() — request dispatch + redirect following │ ├── async_client.rs # AsyncClient — async HTTP client │ ├── async_client_methods.rs # AsyncClient HTTP method macros │ └── async_client_send.rs # AsyncClient.send() — async request dispatch ├── models/ │ ├── mod.rs # Module exports │ ├── cookies.rs # Cookies (domain/path-scoped jar) │ ├── headers.rs # Headers (case-insensitive multidict) │ ├── request.rs # Request object │ ├── response.rs # Response object (core) │ ├── response_properties.rs # Response computed properties (is_success, links, cookies, …) │ └── response_streaming.rs # Response streaming (iter_bytes, iter_lines, iter_text, …) └── transports/ ├── mod.rs # Module exports ├── base.rs # BaseTransport / AsyncBaseTransport traits ├── default.rs # Default transport (reqwest-backed, connection pooling) ├── helpers.rs # Transport helper utilities └── mock.rs # MockTransport / AsyncMockTransport for testing ``` ### Python Package Layout (httpxr/) ``` httpxr/ ├── __init__.py # Public API — re-exports from _httpxr + ASGITransport/WSGITransport ├── __init__.pyi # Top-level type stubs ├── _httpxr.pyi # Full type stubs (1157 lines) for the Rust extension ├── _exceptions.py # Patches HTTPError/RequestError with .request property ├── _transports/ # Python-side transports (WSGI, ASGI) ├── _urlparse.py # urlparse() compatibility helper ├── _utils.py # Python utilities ├── cli.py # CLI entry point (requires `httpxr[cli]` extra) ├── compat.py # httpx compatibility shim (import httpx → import httpxr) └── py.typed # PEP 561 marker ``` ## Complete API Reference ### Top-Level Functions ```python import httpxr # One-shot requests (creates a temporary Client per call) response = httpxr.get(url, **kwargs) response = httpxr.post(url, **kwargs) response = httpxr.put(url, **kwargs) response = httpxr.patch(url, **kwargs) response = httpxr.delete(url, **kwargs) response = httpxr.head(url, **kwargs) response = httpxr.options(url, **kwargs) response = httpxr.request(method, url, **kwargs) response = httpxr.stream(method, url, **kwargs) # Batch concurrent requests without a Client instance responses = httpxr.fetch_all( [{"method": "GET", "url": "https://example.com/1"}, ...], max_concurrency=10, return_exceptions=False, headers=None, timeout=None, verify=True, ) ``` ### Client (Synchronous) ```python client = httpxr.Client( auth=None, # tuple[str,str] | BasicAuth | DigestAuth | Callable params=None, # default query parameters headers=None, # default headers cookies=None, # default cookies timeout=5.0, # float | Timeout | None follow_redirects=False, max_redirects=20, verify=True, # bool | str (CA bundle path) | ssl context cert=None, # client certificate path http2=False, proxy=None, # str | Proxy limits=None, # Limits mounts=None, # {"https://": transport, ...} transport=None, # custom BaseTransport base_url=None, # prepended to relative URLs trust_env=True, default_encoding="utf-8", event_hooks=None, # {"request": [...], "response": [...]} ) # Standard HTTP methods — same signature as httpx response = client.get(url, *, params, headers, cookies, follow_redirects, timeout, extensions) response = client.post(url, *, content, data, files, json, params, headers, ...) response = client.put(url, ...) response = client.patch(url, ...) response = client.delete(url, ...) response = client.head(url, ...) response = client.options(url, ...) response = client.request(method, url, ...) # Streaming response = client.stream(method, url, ...) # context manager # Low-level request = client.build_request(method, url, ...) response = client.send(request, *, auth, follow_redirects) # httpxr Extensions responses = client.gather(requests, max_concurrency=10, return_exceptions=False) pages = client.paginate(method, url, next_url=None, next_header=None, next_func=None, max_pages=100) # Raw API — maximum speed, returns (status: int, headers: dict, body: bytes) status, headers, body = client.get_raw(url, headers=None, timeout=None) status, headers, body = client.post_raw(url, headers=None, body=None, timeout=None) status, headers, body = client.put_raw(url, headers=None, body=None, timeout=None) status, headers, body = client.patch_raw(url, headers=None, body=None, timeout=None) status, headers, body = client.delete_raw(url, headers=None, timeout=None) status, headers, body = client.head_raw(url, headers=None, timeout=None) # Also available: gather_raw(), paginate_raw() client.close() # or use as context manager: with httpxr.Client() as client: ... ``` ### AsyncClient Same API as Client but all request methods are `async`. Additional differences: - `await client.get(url, ...)` - `await client.send(request, ...)` - `await client.gather(requests, ...)` - `async for page in client.paginate(...):` - `await client.aclose()` - `async with httpxr.AsyncClient() as client:` ### URL ```python url = httpxr.URL("https://user:pass@example.com:8080/path?key=val#frag") url.scheme # "https" url.host # "example.com" url.port # 8080 url.path # "/path" url.query # b"key=val" url.fragment # "frag" url.authority # "user:pass@example.com:8080" url.netloc # "example.com:8080" url.userinfo # "user:pass" url.raw_path # b"/path?key=val" url.is_absolute_url # True url.is_relative_url # False url.params # QueryParams({"key": "val"}) url.raw # dict # Immutable copy methods new_url = url.copy_with(scheme="http", path="/new") new_url = url.copy_set_param("key", "new_val") new_url = url.copy_add_param("key2", "val2") new_url = url.copy_remove_param("key") new_url = url.copy_merge_params({"a": "1", "b": "2"}) joined = url.join("/relative/path") ``` ### Headers ```python headers = httpxr.Headers({"Content-Type": "application/json", "Accept": "text/html"}) headers = httpxr.Headers([("key", "val1"), ("key", "val2")]) # multi-valued headers["content-type"] # case-insensitive lookup headers.get("x-custom", "default") headers.get_list("set-cookie", split_commas=False) headers.keys() / .values() / .items() / .multi_items() headers.raw # list[tuple[bytes, bytes]] headers.update({"new-header": "value"}) headers.setdefault("accept", "application/json") headers.copy() ``` ### QueryParams ```python params = httpxr.QueryParams({"page": "1", "limit": "20"}) params = httpxr.QueryParams([("tag", "a"), ("tag", "b")]) # multi-valued params["page"] # "1" params.get("missing", "0") # "0" params.get_list("tag") # ["a", "b"] params.multi_items() # [("tag", "a"), ("tag", "b")] # Immutable operations — return new QueryParams new_params = params.set("page", "2") new_params = params.add("tag", "c") new_params = params.remove("limit") new_params = params.merge({"sort": "name"}) ``` ### Cookies ```python cookies = httpxr.Cookies({"session": "abc123"}) cookies.set("token", "xyz", domain="example.com", path="/api") cookies.get("session") cookies.get("token", domain="example.com") cookies.delete("session") cookies.clear() cookies.update({"new": "cookie"}) cookies.keys() / .values() / .items() cookies.extract_cookies(response) # extract Set-Cookie from a Response cookies.jar # underlying jar object ``` ### Request ```python request = httpxr.Request( method="POST", url="https://example.com/api", params={"key": "val"}, headers={"Authorization": "Bearer token"}, content=b"raw bytes", # or data={"field": "value"}, # or files=[("file", open(...))], # or json={"key": "value"}, extensions={"timeout": {"connect": 5.0}}, ) request.method # "POST" request.url # URL object request.headers # Headers object request.content # bytes (reads body) request.stream # stream object request.read() # sync read await request.aread() # async read ``` ### Response ```python response.status_code # int (e.g. 200) response.headers # Headers response.url # URL response.content # bytes response.text # str (decoded) response.json() # parsed JSON (via simd-json) response.encoding # str | None (settable) response.charset_encoding # from Content-Type header response.request # back-reference to Request response.elapsed # datetime.timedelta response.reason_phrase # "OK" response.http_version # "HTTP/1.1" response.history # list[Response] (redirect chain) response.cookies # Cookies from Set-Cookie headers response.links # parsed Link headers response.next_request # Request | None (pagination) response.extensions # dict (e.g. reason_phrase, http_version) response.num_bytes_downloaded # int # Status helpers response.is_informational # 1xx response.is_success # 2xx response.is_redirect # 3xx response.is_client_error # 4xx response.is_server_error # 5xx response.is_error # 4xx or 5xx response.has_redirect_location # Stream state response.is_closed response.is_stream_consumed # Actions response.raise_for_status() # raises HTTPStatusError on 4xx/5xx response.read() # sync read body await response.aread() # async read body response.close() await response.aclose() # Context manager with response: ... async with response: ... ``` ### Configuration Objects ```python # Timeout — per-phase timeout control timeout = httpxr.Timeout(5.0) # all phases = 5s timeout = httpxr.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0) timeout.connect / timeout.read / timeout.write / timeout.pool # Limits — connection pool limits limits = httpxr.Limits( max_connections=100, max_keepalive_connections=20, keepalive_expiry=5.0, ) # Proxy proxy = httpxr.Proxy( url="http://proxy.example.com:8080", auth=("user", "pass"), headers={"Proxy-Auth": "token"}, ) # RetryConfig (httpxr extension) retry = httpxr.RetryConfig( max_retries=3, backoff_factor=0.5, retry_on_status=[429, 500, 502, 503], jitter=True, ) retry.delay_for_attempt(1) # calculated delay retry.should_retry(429) # True # Defaults httpxr.DEFAULT_TIMEOUT_CONFIG # Timeout(5.0) httpxr.DEFAULT_LIMITS # Limits(...) httpxr.DEFAULT_MAX_REDIRECTS # 20 ``` ### Authentication ```python # Basic Auth auth = httpxr.BasicAuth(username="user", password="pass") client = httpxr.Client(auth=auth) # Digest Auth (MD5, SHA-256 — full RFC 7616) auth = httpxr.DigestAuth(username="user", password="pass") # Tuple shorthand (converted to BasicAuth internally) client = httpxr.Client(auth=("user", "pass")) ``` ### Transports ```python # Mock transport — for testing def handler(request: httpxr.Request) -> httpxr.Response: return httpxr.Response(200, json={"mock": True}) transport = httpxr.MockTransport(handler) client = httpxr.Client(transport=transport) # ASGI transport — test ASGI apps without a server transport = httpxr.ASGITransport(app=my_asgi_app) client = httpxr.Client(transport=transport, base_url="http://testserver") # WSGI transport — test WSGI apps without a server transport = httpxr.WSGITransport(app=my_wsgi_app) client = httpxr.Client(transport=transport, base_url="http://testserver") # Transport mounts — route requests to different transports client = httpxr.Client(mounts={ "https://api.example.com": custom_transport, "all://": default_transport, }) # Base classes for custom transports class MyTransport(httpxr.BaseTransport): def handle_request(self, request): ... class MyAsyncTransport(httpxr.AsyncBaseTransport): async def handle_async_request(self, request): ... ``` ### Streaming ```python # Sync streaming with client.stream("GET", url) as response: for chunk in response.iter_bytes(chunk_size=1024): process(chunk) # or for line in response.iter_lines(): process(line) # or for text_chunk in response.iter_text(): process(text_chunk) # Async streaming async with await async_client.request("GET", url, stream=True) as response: async for chunk in response.aiter_bytes(): process(chunk) # Stream types stream = httpxr.ByteStream(b"raw data") ``` ### Exception Hierarchy ``` Exception ├── InvalidURL ├── CookieConflict └── HTTPError ├── HTTPStatusError (.response, .request) └── RequestError (.request) ├── DecodingError ├── TooManyRedirects ├── StreamError │ ├── StreamConsumed │ ├── StreamClosed │ ├── ResponseNotRead │ └── RequestNotRead └── TransportError ├── TimeoutException │ ├── ConnectTimeout │ ├── ReadTimeout │ ├── WriteTimeout │ └── PoolTimeout ├── NetworkError │ ├── ConnectError │ ├── ReadError │ ├── WriteError │ └── CloseError ├── ProxyError ├── UnsupportedProtocol └── ProtocolError ├── LocalProtocolError └── RemoteProtocolError ``` ### Status Codes ```python httpxr.codes.OK # 200 httpxr.codes.NOT_FOUND # 404 httpxr.codes.INTERNAL_SERVER_ERROR # 500 # ... all standard HTTP status codes # Helper methods on codes instances codes.is_informational() # 1xx codes.is_success() # 2xx codes.is_redirect() # 3xx codes.is_client_error() # 4xx codes.is_server_error() # 5xx codes.is_error() # 4xx or 5xx ``` ### Utility Functions ```python httpxr.primitive_value_to_str(value) httpxr.to_bytes(data) httpxr.to_str(data) httpxr.to_bytes_or_str(data) httpxr.unquote(data) httpxr.peek_filelike_length(file) httpxr.is_ipv4_hostname(hostname) httpxr.is_ipv6_hostname(hostname) httpxr.get_environment_proxies() httpxr.encode_request(content, data, files, json) httpxr.create_ssl_context(verify, cert, trust_env) ``` ### Decoder Types ```python decoder = httpxr.PyDecoder(encoding="utf-8") # identity / gzip / brotli / zstd data = decoder.decode(compressed_bytes) remaining = decoder.flush() chunker = httpxr.ByteChunker(chunk_size=1024) chunks = chunker.decode(data) remaining = chunker.flush() line_decoder = httpxr.LineDecoder() lines = line_decoder.decode("partial text\nmore") remaining = line_decoder.flush() ``` ### Multipart Upload ```python stream = httpxr.MultipartStream( data={"field1": "value1"}, files=[("file", ("report.pdf", open("report.pdf", "rb"), "application/pdf"))], boundary=None, # auto-generated ) stream.content_type() # "multipart/form-data; boundary=..." stream.get_content() # full body as bytes stream.get_content_length() ``` ## httpxr-Only Extensions (Not in httpx) 1. **`gather()`** — dispatch a list of pre-built Requests concurrently via Rust's tokio runtime. 2. **`paginate()`** — auto-follow pagination links (JSON body key, Link header, or custom function). 3. **`*_raw()` methods** — bypass Python Request/Response construction; return `(int, dict, bytes)`. 4. **`gather_raw()` / `paginate_raw()`** — raw versions of gather and paginate. 5. **`fetch_all()`** — top-level concurrent batch requests without creating a Client. 6. **`RetryConfig`** — built-in retry with exponential backoff, jitter, and status-code filtering. 7. **`compat` module** — `httpxr.compat.install()` patches `import httpx` to resolve to `httpxr`. ## httpx Compatibility Shim ```python # Redirect all `import httpx` to `import httpxr` import httpxr.compat httpxr.compat.install() # Now any library that does `import httpx` will get httpxr import httpx # → actually httpxr ``` ## Development Workflow ```bash # Clone and setup git clone https://github.com/bmsuisse/httpxr.git cd httpxr uv sync --group dev # Build the Rust extension maturin develop # Run tests (1303 tests from the full httpx suite) uv run pytest tests/ # Type checking uv run pyright # Linting uv run ruff check . # Run benchmarks uv sync --group benchmark uv run python benchmarks/run_benchmark.py # Build docs uv sync --group docs uv run mkdocs serve ``` ### Release Build Optimizations (Cargo.toml) ```toml [profile.release] opt-level = 3 # maximum optimization codegen-units = 1 # single codegen unit for better LTO lto = "fat" # full link-time optimization strip = true # strip debug symbols panic = "abort" # smaller binary (no unwinding) ``` ### Key Rust Dependencies - **pyo3** 0.28 — Python bindings (extension-module, multiple-pymethods) - **pyo3-async-runtimes** 0.28 — tokio runtime integration - **tokio** 1 — async runtime (rt-multi-thread, macros, sync, time) - **reqwest** 0.13 — HTTP client (rustls, json, cookies, gzip, brotli, zstd, deflate, stream, socks) - **serde_json** 1.0 — JSON serialization (with raw_value) - **simd-json** 0.17 — SIMD-accelerated JSON parsing - **mimalloc** — high-performance memory allocator - **url** 2 — URL parsing - **idna** 1.1 — internationalized domain names - **percent-encoding** 2 — URL percent-encoding - **md-5**, **sha1**, **sha2** — digest auth hashing - **base64** 0.22 — base64 encoding - **futures** 0.3 — async utilities - **bytes** 1 — efficient byte buffers ## Test Suite - **1303 tests** ported 1:1 from the httpx test suite. - Covers: clients, models, transports, streaming, auth flows, redirects, multipart, cookies, edge cases. - Test framework: pytest with pytest-timeout. - Test transports: MockTransport, ASGI (uvicorn), trustme (SSL). ## Examples (examples/) ``` examples/ ├── async_client.py # AsyncClient basics ├── async_gather.py # concurrent async requests ├── authentication.py # BasicAuth, DigestAuth ├── basic_requests.py # simple GET/POST ├── client_usage.py # Client lifecycle ├── error_handling.py # exception handling patterns ├── gather.py # sync concurrent requests ├── headers_and_params.py # headers, query params ├── json_and_forms.py # JSON and form data ├── mock_transport.py # testing with MockTransport ├── paginate.py # auto-pagination ├── streaming.py # streaming responses └── timeouts_and_limits.py # timeout/limits configuration ``` ## Documentation Site (docs/) Full MkDocs Material site at https://bmsuisse.github.io/httpxr/: - Quickstart, Clients, Requests & Responses - Headers, Params & Cookies - Authentication (Basic, Digest) - Streaming, Timeouts & Limits - Extensions (gather, paginate, raw API) - Testing (MockTransport, ASGI/WSGI) - CLI reference - Migration guide (httpx → httpxr) - Cookbook (common recipes) - Compatibility shim - Benchmarks (interactive Plotly charts) - Error handling - How it was built (AI development story) ## Common Tasks - **Adding a feature**: Implement in Rust (src/), expose via `#[pymethod]` or `#[pyfunction]`, update `_httpxr.pyi` type stubs, add test. - **Fixing a bug**: Find the corresponding httpx test in `tests/`, reproduce failure, fix in Rust, rebuild with `maturin develop`. - **Adding an example**: Create a new `.py` file in `examples/`. - **Adding documentation**: Create/edit `.md` files in `docs/`, update `mkdocs.yml` nav.