| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Certifies httpware under free-threaded CPython (PEP 703), backed by CI
evidence rather than a bare classifier.
httpware's resilience suite is built on threading.Lock/Semaphore and a
shared client across threads is a documented usage pattern; free-threaded
CPython removes the GIL that currently masks latent races. This release adds
the missing proof: a committed contention benchmark
(benchmarks/contention.py, planning/audits/2026-07-18-free-threading-audit.md)
found free-threaded 3.14t ~1.9x slower than GIL 3.11 for RetryBudget's
single-shared-lock hot loop — lock contention dominates that access pattern.
Free-threading support here is a correctness certification, not a
performance claim.
No API changes. Safe to upgrade unconditionally; the classifier and CI job
are the only visible additions.
No public API changes. Every commit since 0.15.1 is docs, internal refactoring,
CI, or planning tooling — safe to upgrade unconditionally.
Continuation of the sync/async deduplication pass flagged by the 2026-06-14
deep audit — every remaining "duplicated, no divergence yet" item from that
audit is now closed:
No action required. Nothing about the installed package's public behavior
changes.
No library changes. The package is identical to 0.15.0; this release exercises the new publish path end-to-end.
No action required. Nothing about the installed package changes.
Minor release. Contains one breaking change (pre-1.0): the opt-in
max_error_body_bytes parameter is replaced by max_response_body_bytes.
This release turns the error-only body guard into a real, status-agnostic memory
cap that is actually enforced on the non-streaming send() path and against
compression bombs.
max_error_body_bytes is removed and replaced by max_response_body_bytes
on both Client and AsyncClient. There is no compatibility alias — passing the
old keyword raises TypeError.
# before
client = AsyncClient(max_error_body_bytes=1_000_000)
# after
client = AsyncClient(max_response_body_bytes=1_000_000)None (the default) remains unbounded. A non-None value below 1 is now
rejected with ValueError at construction.
The old max_error_body_bytes only fired inside stream(), only on 4xx/5xx, and
only as a declared-Content-Length pre-check. For a non-streaming send(),
httpx2 buffered the whole body before httpware got control, so the hot path had
no cap at all — and a small compressed body could decode to something enormous
(a 133-byte gzip body decodes to 100 KB; real bombs run ~1000:1) and slip past a
header check entirely.
max_response_body_bytes:
The declared Content-Length is kept only as an early reject (never an early
accept), so chunked and bomb bodies are always run through the accumulator.
All public API is honored — no httpx2 private access.
Minor release. Additive only — no breaking changes.
This release adds a read-only state property and a public CircuitState enum
to both AsyncCircuitBreaker and CircuitBreaker, enabling health checks,
readiness probes, dashboards, and test assertions against the current circuit
state without any impact on circuit behavior.
Both breakers now expose a state property that returns one of three values from
the new CircuitState enum (CLOSED, OPEN, HALF_OPEN). The enum is exported
from the top-level httpware package:
from httpware import CircuitState
from httpware.middleware.resilience import AsyncCircuitBreaker
breaker = AsyncCircuitBreaker(failure_threshold=5)
# In a health or readiness handler:
if breaker.state is CircuitState.OPEN:
... # report the dependency as degradedThe same property exists on the sync CircuitBreaker.
state is a raw read of the stored state. The OPEN→HALF_OPEN transition is lazy:
it fires on the next request admitted after reset_timeout elapses, not on a
clock tick. This means state will report OPEN until a request is actually
admitted as the probe — reading the property never triggers the transition.
This is intentional. A health endpoint that polls state cannot accidentally
promote the circuit to HALF_OPEN by reading it; only real traffic does.
The following remain deferred and are not part of 0.14.0:
PR #70 — read-only circuit-breaker state introspection.
Minor release. Additive only — no breaking changes.
This release adds an opt-in time-based failure-rate trip mode to both
AsyncCircuitBreaker and CircuitBreaker. Classic consecutive-failure behavior
is the default and is unchanged.
The classic circuit breaker trips after failure_threshold consecutive counted
failures — a simple and effective policy for hard outages. It can miss partial
degradation, though: a downstream returning errors on half of all requests may
never form a long enough consecutive streak to trip the circuit.
Rate mode addresses this. Pass failure_rate_threshold to switch:
from httpware import AsyncClient
from httpware.middleware.resilience import AsyncCircuitBreaker
breaker = AsyncCircuitBreaker(
failure_rate_threshold=0.5, # open at ≥50% failures
window_seconds=30.0, # over a rolling 30s window
minimum_calls=20, # but only once 20+ calls are observed
)
async with AsyncClient(
base_url="https://api.example.com",
middleware=[breaker],
) as client:
response = await client.get("/users/1")The circuit opens when the observed failure rate over the rolling window_seconds
window meets or exceeds failure_rate_threshold — but only once minimum_calls
outcomes have been recorded in that window. The minimum_calls guard prevents a
single early failure from immediately tripping the circuit before a meaningful
sample has accumulated.
Both AsyncCircuitBreaker and CircuitBreaker accept three new keyword
arguments:
| Parameter | Default | Effect |
|---|---|---|
| failure_rate_threshold | None | Float in (0, 1]. When set, switches the breaker to rate mode. None keeps classic consecutive-failure mode. ≤0 or >1 raises ValueError. |
| window_seconds | 30.0 | Rolling window width for rate mode. Ignored in classic mode. ≤0 raises ValueError. |
| minimum_calls | 20 | Minimum outcomes in the window before the rate is evaluated. Ignored in classic mode. <1 raises ValueError. |
In rate mode, failure_threshold is ignored — the trip condition is purely
rate-based. All other parameters (reset_timeout, success_threshold,
failure_status_codes) apply in both modes.
Event names are identical in both modes: circuit.opened, circuit.rejected,
circuit.half_open, circuit.closed. In rate mode the circuit.opened event
carries additional attributes — failure_rate, failure_rate_threshold,
window_seconds, observed_calls — and its message is
"circuit opened — failure rate threshold reached".
The following remain deferred and are not part of 0.13.0:
PR #69 — time-based failure-rate trip mode for the circuit breaker.
Minor release. Additive only — no breaking changes.
This release adds ergonomic per-verb shortcuts for the common pattern of needing
both the raw httpx2.Response (headers, status, request URL) and a typed body in
a single call — without having to pair build_request(...) with
send_with_response(...).
Six methods on both AsyncClient and Client:
from httpware import AsyncClient
client = AsyncClient(base_url="https://api.example.com", decoders=[...])
# One call — response metadata and typed body together
response, users = await client.get_with_response(
"/users", params={"page": 2}, response_model=list[User]
)
next_url = response.headers.get("Link")
etag = response.headers.get("ETag")| Method | Verb |
|---|---|
| get_with_response | GET |
| post_with_response | POST |
| put_with_response | PUT |
| patch_with_response | PATCH |
| delete_with_response | DELETE |
| request_with_response | any |
Signature: each method requires response_model (keyword-only) and returns
tuple[httpx2.Response, T]. All other kwargs (params, headers, json,
content, timeout, …) pass through to httpx2 unchanged — identical to the
non-_with_response siblings.
Use case: response metadata alongside a typed body — Link-header pagination,
ETag caching, rate-limit reads (X-RateLimit-Remaining), request URL logging on
redirect. When you don't need the raw response, the existing get / post /
… methods remain the preferred form.
Scope: no head_with_response or options_with_response — HEAD is
bodiless and OPTIONS is rarely decoded. request_with_response(method, url, …)
is the escape hatch for any other verb.
Inherited behavior: MissingDecoderError is raised before the HTTP call
when no decoder claims the model type; DecodeError wraps a failure inside
decode(). Both propagate unchanged through the new methods, identical to
send_with_response.
PR #68 — per-verb *_with_response siblings on AsyncClient and Client
(src/httpware/client.py).
Minor release. Additive only — no breaking changes.
This release ships the full remediation of the 2026-06-14 full-codebase deep
audit: 35 confirmed findings closed across security, correctness, public API,
test quality, and documentation.
from httpware import ResponseTooLargeErrorAsyncClient / Client gain an opt-in max_error_body_bytes: int | None = None.
PRs #62 (pydantic isolation), #63 (security cluster), #64 (correctness +
public API), #65 (test quality), #66 (docs). See
planning/audits/2026-06-14-deep-audit.md
for the full audit.
Patch release. Bug fixes + hardening from the 0.10.0 delta audit. No breaking changes (one additive observability field; see below).
Audit report: planning/audit/2026-06-13-delta-audit.md.
Hardened to assert the stable event-name strings, the exact retry_after value, the 429-resets-the-failure-streak path, success_threshold > 1 with a mid-streak probe failure, and reset_timeout=0 / empty failure_status_codes boundaries. No production behavior change from the test work.
Minor release. Additive only — no breaking changes.
from httpware.middleware.resilience import AsyncCircuitBreaker # async
from httpware.middleware.resilience import CircuitBreaker # sync
from httpware.middleware.resilience import AsyncTimeout
from httpware import CircuitOpenErrorClassic consecutive-failure circuit breaker. Counts counted failures (5xx, NetworkError, TimeoutError) and fast-fails with CircuitOpenError once failure_threshold consecutive failures are observed. Recovers via a HALF_OPEN probe after reset_timeout seconds; closes when success_threshold consecutive probe successes are seen.
4xx responses — including 429 — count as successes. A 429 means healthy-but-throttling; tripping the circuit on it would amplify incidents.
CircuitOpenError (a ClientError subclass) carries retry_after: float | None — the seconds until the next probe window (None when HALF_OPEN with a probe already in flight).
Sharable across multiple clients (one shared circuit). A sync CircuitBreaker cannot be shared with an AsyncCircuitBreaker.
Bounds total wall-clock across the inner pipeline — including retries and backoff sleeps. Raises httpware.TimeoutError on expiry. Async-only: sync Python has no cancellation primitive that can interrupt a blocking call mid-flight.
| Logger | Event | When |
|---|---|---|
| httpware.circuit_breaker | circuit.opened | Failure threshold reached |
| httpware.circuit_breaker | circuit.rejected | Request fast-failed (OPEN or HALF_OPEN probe taken) |
| httpware.circuit_breaker | circuit.half_open | Reset timeout elapsed; probe admitted |
| httpware.circuit_breaker | circuit.closed | Success threshold reached; service recovered |
| httpware.timeout | timeout.exceeded | Overall timeout expired |
AsyncTimeout → AsyncCircuitBreaker → AsyncBulkhead → AsyncRetry → terminal
Breaker outside retry: an open circuit short-circuits the whole retry loop; the breaker counts one outcome per fully-exhausted retry sequence.
| Back | FazBrowse Home | New Git URL |