httpware¶
A Python HTTP client framework with sync and async clients for building resilient service clients. httpware is a thin opinionated wrapper around httpx2 — it re-exports httpx2.Request/httpx2.Response as the public request/response surface, adds a middleware chain (with a built-in resilience suite: AsyncRetry/Retry + RetryBudget, AsyncBulkhead/Bulkhead), opt-in typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
Status: Pre-1.0. Public API is subject to change between minor releases until v1.0.
Install¶
Optional extras:
pip install httpware[pydantic] # PydanticDecoder (the default decoder path)
pip install httpware[msgspec] # MsgspecDecoder
First request¶
Async usage:
import asyncio
from httpware import AsyncClient
async def main() -> None:
async with AsyncClient(base_url="https://example.test") as client:
response = await client.get("/users/42")
print(response.json())
asyncio.run(main())
Sync usage:
from httpware import Client
with Client(base_url="https://example.test") as client:
response = client.get("/users/42")
print(response.json())
Typed decoding via response_model= works the same way in both worlds:
from httpware import AsyncClient
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
async def main() -> None:
async with AsyncClient(base_url="https://api.example.com") as client:
user = await client.get("/users/1", response_model=User)
print(user.name)
With resilience middleware¶
Compose resilience middleware at construction; AsyncBulkhead goes outside AsyncRetry so one slot covers all retry attempts.
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry
async def main() -> None:
async with AsyncClient(
base_url="https://api.example.com",
middleware=[
AsyncBulkhead(max_concurrent=10), # cap total in-flight
AsyncRetry(), # default: 3 attempts, full-jitter backoff
],
) as client:
user = await client.get("/users/1", response_model=User)
Response metadata + typed body¶
When you need both the raw httpx2.Response (for headers, status, or the
request URL) and a typed body, use send_with_response. It returns
both atomically and routes the decode through the configured
ResponseDecoder, so decoder failures surface as DecodeError — caught
by except httpware.ClientError like every other failure mode.
Canonical use case: RFC 5988 Link header pagination.
Assume process and next_link are caller-defined — pick a Link header parser that fits.
from httpware import AsyncClient
from pydantic import BaseModel
class Tag(BaseModel):
name: str
async def main() -> None:
async with AsyncClient(base_url="https://gitlab.example/api/v4") as client:
url = "/projects/1/repository/tags"
params: dict[str, str] | None = {"per_page": "100", "page": "1"}
while url:
request = client.build_request("GET", url, params=params)
response, tags = await client.send_with_response(request, response_model=list[Tag])
for tag in tags:
process(tag)
url = next_link(response.headers.get("link")) # caller's parser
params = None # next link carries query
For body-only with a high-level verb, prefer client.get(..., response_model=...).
For body-only with a custom Request, prefer client.send(request, response_model=...).
send_with_response is not for streaming responses — use stream().
Streaming responses¶
For large responses or server-sent events, stream the body chunk-by-chunk. stream() is an async context manager:
from httpware import AsyncClient
async def main() -> None:
async with AsyncClient(base_url="https://api.example.com") as client:
async with client.stream("GET", "/big-file") as response:
async for chunk in response.aiter_bytes():
process(chunk)
stream() auto-raises StatusError subclasses on 4xx/5xx with the response body pre-read, so exc.response.content is accessible from the caught exception.
It does NOT pass through the middleware chain: AsyncRetry, AsyncBulkhead, and any custom middleware are bypassed. (AsyncRetry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
Errors¶
All 4xx/5xx responses raise typed exceptions automatically: NotFoundError, ServiceUnavailableError, RateLimitedError, etc. — all subclasses of httpware.StatusError. Transport-layer transient failures raise NetworkError; the resilience middleware raise RetryBudgetExhaustedError and BulkheadFullError. Everything inherits httpware.ClientError.
Observability¶
AsyncRetry/Retry and AsyncBulkhead/Bulkhead emit operational events via two channels — stdlib logging records (always on) and OpenTelemetry span events (when opentelemetry-api is installed). Event names and payloads are identical across sync and async; dashboards built against one class apply unchanged to the other.
Logger names (httpware.retry, httpware.bulkhead) and event names (retry.giving_up, retry.budget_refused, retry.streaming_refused, bulkhead.rejected) are the stable public contract.
import logging
# Enable visibility into retry / bulkhead operational events
logging.getLogger("httpware.retry").setLevel(logging.WARNING)
logging.getLogger("httpware.bulkhead").setLevel(logging.WARNING)
For OTel attribute enrichment on the active span — install the extra:
When installed, _emit_event calls trace.get_current_span().add_event(name, attributes=...) automatically. We never create our own spans; for HTTP-level tracing install opentelemetry-instrumentation-httpx separately.
Where to go next¶
- Resilience reference — every parameter on
AsyncRetry,RetryBudget, andAsyncBulkhead; the retry-rule matrix; Retry-After parsing; budget sharing. - Middleware guide — write your own middleware. Covers the AsyncMiddleware Protocol, the phase decorators, a worked Request-ID propagation example, and OpenTelemetry wiring.
- Errors reference — the full exception tree, catching strategies,
exc.response.*access pattern. - Testing guide — mock-transport injection pattern for testing code that uses
httpware. - Recipes — wiring
AsyncClientinto amodern-dicontainer. - Engineering Notes — design invariants, the three protocol seams, exception contract, module layout, testing patterns, optional-extras pattern. Lives in the repo at
planning/engineering.md. - Contributing — setup, conventions, workflow.
- Release notes — per-version changelogs.
Part of modern-python¶
httpware ships under the modern-python org. See the org profile for the categorized index of related templates and libraries.