CLAUDE.mdpython
httpr CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
httpr is a high-performance HTTP client for Python built in Rust using PyO3 and reqwest. It's designed as a drop-in replacement for httpx and requests with significantly better performance.
Build & Development Commands
# Install development dependencies
uv sync --extra dev
# Build Rust extension (required after any Rust code changes)
uv run maturin develop
# Run tests (depends on httpbin.org)
uv run pytest tests/
# Type checking
uv run mypy httpr/
# Linting
uv run ruff check httpr/
Pre-commit Hooks
Set up pre-commit hooks to automatically lint and format code before commits:
# Install pre-commit
uv pip install pre-commit
# Install git hooks
pre-commit install
pre-commit install --hook-type commit-msg
# Run all hooks manually (optional)
pre-commit run --all-files
Configured hooks (see .pre-commit-config.yaml):
- ruff: Python linting and formatting
- mypy: Python type checking (httpr/ only)
- cargo fmt/clippy: Rust formatting and linting
- commitizen/commitlint: Conventional commit messages
Taskfile Commands
This project uses Taskfile for development workflows:
task --list # List all available tasks
# Testing
task test:unit # Run unit tests only
task test:e2e # Run e2e tests (requires httpbun running)
task e2e # Full e2e workflow: certs → start httpbun → test → stop
task e2e:local # Start httpbun and run e2e tests (keep container running)
# Development
task dev # Build Rust extension
task check # Run all checks (lint + test) - use before committing
# Linting
task lint # Run Python linters (ruff + mypy)
task lint:rust # Run Rust linters (fmt + clippy)
task lint:all # Run all linters (Python + Rust)
# Formatting
task fmt # Format Python code
task fmt:rust # Format Rust code
task fmt:all # Format all code
E2E Tests with httpbun
E2E tests use httpbun in Docker with SSL certificates generated by trustme.
# One-time setup: add hosts entry
echo '127.0.0.1 httpbun.local' | sudo tee -a /etc/hosts
# Run full e2e workflow
task e2e
# Or run iteratively during development
task e2e:local # Starts httpbun and runs tests (keeps container running)
task test:e2e # Run e2e tests again
task httpbun:stop # Stop container when done
Key files:
Taskfile.yaml- Task definitionsscripts/generate_certs.py- SSL cert generation using trustmetests/e2e/- E2E test files.certs/- Generated certificates (gitignored)
Benchmarking
cd benchmark/
uv run uvicorn server:app # Terminal 1: Start test server
uv run python benchmark.py # Terminal 2: Run benchmarks
Architecture
Rust Core (src/)
lib.rs: MainRClientclass with sync request handling via single-threaded Tokio runtime (LazyLock<Runtime>withnew_current_thread())request()method: Buffers entire response body_stream()method: ReturnsStreamingResponsewithout buffering body
response.rs: Response objects withCaseInsensitiveHeaderMapfor HTTP/2 compliant header handlingResponse: Standard response with bufferedcontentStreamingResponse: HoldsArc<Mutex<Option<reqwest::Response>>>for chunk iterationTextIterator: Iterator for decoding chunks as textLineIterator: Iterator for line-by-line reading with internal buffer
traits.rs: Conversion traits between Python/Rust types (IndexMap ↔ HeaderMap)utils.rs: CA certificate loading, encoding detection
Python Wrapper (httpr/)
__init__.py:Client(sync) andAsyncClientclasses with context manager supportstream()context manager wraps_stream()and handles cleanup- Both
ClientandAsyncClientsupport streaming
AsyncClientusesasyncio.run_in_executor()to wrap sync Rust calls - NOT native asynchttpr.pyi: Type stubs for IDE support includingStreamingResponse,TextIterator,LineIterator
Key Design Decisions
- Single Tokio Runtime: All async Rust operations run on one thread
- Async is Sync:
AsyncClientruns sync Rust code in thread executor - Zero Python Dependencies: All functionality in Rust
- Case-Insensitive Headers: Custom struct maintains original casing while allowing case-insensitive lookups (HTTP/2 requirement)
- Streaming:
StreamingResponseholds reqwest response and provides chunk iteration without buffering entire body
Critical Implementation Details
Python-Rust Interface
- All Python params converted to strings before passing to Rust
- HTTP method validation happens in Python wrapper
- Rust uses
IndexMap<String, String, RandomState>(foldhash) for dicts - Use
Unpackfor**kwargstyping (via typing_extensions for Python ≤3.11)
SSL/TLS
- CA certs loaded via
HTTPR_CA_BUNDLEenv var ca_cert_fileparam setsHTTPR_CA_BUNDLEinternally- mTLS via
client_pemparameter (file path, PEM format) orclient_pem_data(bytes, PEM format) client_pem_dataallows passing certificates without filesystem access (useful for containers/secrets managers)verify=Falseenablesdanger_accept_invalid_certs()
Headers Behavior
- Headers are lowercased internally (HTTP/2 spec)
client.headersgetter excludesCookieheaderclient.cookiesgetter/setter extracts fromCookieheader
Request Body
- Mutually exclusive:
content(bytes),data(form),json(JSON),files(multipart) dataandjsonusepythonize::depythonize()for Python → Rust conversionfilesdict maps field names to file paths
Proxy
- Set via
proxyparam orHTTPR_PROXYenv var - Changing
client.proxyrebuilds entire reqwest client (expensive)
Streaming Responses
_stream()method returnsStreamingResponsewithout calling.bytes()on reqwest responseStreamingResponseholdsArc<Mutex<Option<reqwest::Response>>>to allow chunk reading across Python GIL boundaries- Chunk iteration uses
RUNTIME.block_on()withpy.allow_threads()to read each chunk - State tracking via
Arc<Mutex<bool>>forclosedandconsumedflags - Three iteration modes:
iter_bytes(): Direct chunk iteration (returnsIterator[bytes])iter_text(): ReturnsTextIteratorthat decodes chunks using response encodingiter_lines(): ReturnsLineIteratorwith internal buffer for line-by-line reading
read()method consumes remaining response body and marks as consumedclose()method sets closed flag and drops the response- Python wrapper uses
@contextmanagerto ensureclose()is called on exit - AsyncClient streaming: Context manager is async, but iteration is sync (same as sync Client)
What NOT to Do
- Don't add Python dependencies (defeats "zero dependencies" goal)
- Don't use native async Rust in request path (breaks single-threaded runtime model)
- Don't modify headers case-sensitivity behavior (HTTP/2 spec requirement)
- Don't skip
maturin developafter Rust changes