Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

**Async HTTP client framework for Python.**

`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain composed at client construction, supports opt-in typed response decoding (pydantic and msgspec are both extras), and raises a status-keyed exception tree automatically on 4xx/5xx. It also ships a small resilience suite — `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — under `httpware.middleware.resilience`.
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request`/`httpx2.Response`, adds a middleware chain composed at client construction, supports opt-in typed response decoding (pydantic and msgspec are both extras), and raises a status-keyed exception tree automatically on 4xx/5xx. It also ships a small resilience suite — `AsyncRetry` middleware with a Finagle-style `RetryBudget`, plus an `AsyncBulkhead` concurrency limiter — under `httpware.middleware.resilience`.

> **Status:** Pre-1.0. Public API is subject to change between minor releases until v1.0.

Expand Down Expand Up @@ -44,18 +44,18 @@ async def main() -> None:

### With resilience middleware

Compose resilience middleware at construction; `Bulkhead` goes outside `Retry` so one slot covers all retry attempts.
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.

```python
from httpware import AsyncClient, Bulkhead, Retry
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry


async def main() -> None:
async with AsyncClient(
base_url="https://api.example.com",
middleware=[
Bulkhead(max_concurrent=10), # cap total in-flight
Retry(), # default: 3 attempts, full-jitter backoff
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)
Expand All @@ -80,15 +80,15 @@ async def main() -> None:

`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: `Retry`, `Bulkhead`, and any custom middleware are bypassed. (Retry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
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

`Retry` and `Bulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).
`AsyncRetry` and `AsyncBulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).

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.

Expand Down
8 changes: 4 additions & 4 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ For the resilience-specific errors (`RetryBudgetExhaustedError`, `BulkheadFullEr
```
ClientError (catch-all for anything httpware raises)
├── TransportError (connection/network/protocol failure pre-response)
│ └── NetworkError (transient — safe to retry; covered by Retry's defaults)
│ └── NetworkError (transient — safe to retry; covered by AsyncRetry's defaults)
├── TimeoutError (also inherits builtins.TimeoutError — except OSError catches it)
├── StatusError (got a response but its status was 4xx/5xx)
│ ├── ClientStatusError (any 4xx — fallback for unknown 4xx codes)
Expand Down Expand Up @@ -72,7 +72,7 @@ async def fetch(client: AsyncClient, user_id: int) -> dict | None:
_LOGGER.warning("upstream returned %s for %s", exc.response.status_code, exc.response.request.url)
raise
except NetworkError:
# Transient transport failure. Already retried by the default Retry middleware
# Transient transport failure. Already retried by the default AsyncRetry middleware
# (if installed) when the method was idempotent. Seeing this means retries
# exhausted or the method was non-idempotent.
raise
Expand Down Expand Up @@ -128,6 +128,6 @@ except RetryBudgetExhaustedError as exc:

## See also

- **[Resilience reference](resilience.md)** — `Retry`, `RetryBudget`, `Bulkhead` parameter tables.
- **[Middleware guide](middleware.md)** — the `@on_error` decorator can translate exceptions into responses.
- **[Resilience reference](resilience.md)** — `AsyncRetry`, `RetryBudget`, `AsyncBulkhead` parameter tables.
- **[Middleware guide](middleware.md)** — the `@async_on_error` decorator can translate exceptions into responses.
- **`planning/engineering.md` §4** — the formal exception contract.
18 changes: 9 additions & 9 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# httpware

A Python async HTTP client framework 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: `Retry` + `RetryBudget`, `Bulkhead`), opt-in typed response decoding, and a status-keyed exception tree raised automatically on 4xx/5xx.
A Python async HTTP client framework 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` + `RetryBudget`, `AsyncBulkhead`), 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.

Expand Down Expand Up @@ -42,18 +42,18 @@ asyncio.run(main())

### With resilience middleware

Compose resilience middleware at construction; `Bulkhead` goes outside `Retry` so one slot covers all retry attempts.
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.

```python
from httpware import AsyncClient, Bulkhead, Retry
from httpware import AsyncClient, AsyncBulkhead, AsyncRetry


async def main() -> None:
async with AsyncClient(
base_url="https://api.example.com",
middleware=[
Bulkhead(max_concurrent=10), # cap total in-flight
Retry(), # default: 3 attempts, full-jitter backoff
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)
Expand All @@ -76,15 +76,15 @@ async def main() -> None:

`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: `Retry`, `Bulkhead`, and any custom middleware are bypassed. (Retry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
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

`Retry` and `Bulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).
`AsyncRetry` and `AsyncBulkhead` emit operational events via two channels — stdlib `logging` records (always on) and OpenTelemetry span events (when `opentelemetry-api` is installed).

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.

Expand All @@ -106,8 +106,8 @@ When installed, `_emit_event` calls `trace.get_current_span().add_event(name, at

## Where to go next

- **[Resilience reference](resilience.md)** — every parameter on `Retry`, `RetryBudget`, and `Bulkhead`; the retry-rule matrix; Retry-After parsing; budget sharing.
- **[Middleware guide](middleware.md)** — write your own middleware. Covers the Middleware Protocol, the phase decorators, a worked Request-ID propagation example, and OpenTelemetry wiring.
- **[Resilience reference](resilience.md)** — every parameter on `AsyncRetry`, `RetryBudget`, and `AsyncBulkhead`; the retry-rule matrix; Retry-After parsing; budget sharing.
- **[Middleware guide](middleware.md)** — write your own middleware. Covers the AsyncMiddleware Protocol, the phase decorators, a worked Request-ID propagation example, and OpenTelemetry wiring.
- **[Errors reference](errors.md)** — the full exception tree, catching strategies, `exc.response.*` access pattern.
- **[Testing guide](testing.md)** — mock-transport injection pattern for testing code that uses `httpware`.
- **[Recipes](recipes/modern-di.md)** — wiring `AsyncClient` into a `modern-di` container.
Expand Down
Loading
Loading