Releases: modern-python/httpware
Release list
0.21.0
Behaviour change: when a URL already has a query string, params (client-level, then per-request) are now appended after it, matching requests. A key present in both is sent twice rather than replaced: ?a=1&b=1 with params={"b": "2"} now sends a=1&b=1&b=2 (previously a=1&b=2). The URL's own query is also sent byte for byte instead of re-encoded (?q=a%20b no longer becomes q=a+b). See #156.
What's Changed
- docs: refresh the social card with the current description by @lesnik512 in #154
- docs: document the blocked label by @lesnik512 in #155
- test: bound the HTTP-date Retry-After delay by the call's own clock window by @lesnik512 in #157
- fix!: append params after the URL's own query and keep its bytes by @lesnik512 in #156
Full Changelog: 0.20.1...0.21.0
0.20.1
What's Changed
- fix: keep multi-step auth responses under the response body cap by @lesnik512 in #153
Body cap fix
Before this release, an auth= that takes more than one step got around max_response_body_bytes: httpx2 reads every intermediate auth response without a limit. That covers the 401 challenge of httpx2.DigestAuth and custom flows such as a token refresh that calls a token endpoint and retries. With a cap set, httpware now runs the auth flow itself. Intermediate auth responses are closed without reading their bodies; an auth that sets requires_response_body gets each response read under the cap, so an oversized one raises ResponseTooLargeError. Without a cap nothing changes.
With a cap set, these now match httpx2's own behaviour:
DigestAuthanswers a challenge that arrives after a redirect (0.20.0 could not).- Credentials written into a redirect
LocationURL are not applied. - Auth steps count toward
max_redirects. - With an auth that sets
requires_response_body,stream()yields a body already read under the cap.
Full Changelog: 0.20.0...0.20.1
0.20.0
What's Changed
- feat: follow redirects under the response body cap by @lesnik512 in #152
Behaviour change
follow_redirects=True together with max_response_body_bytes no longer raises ValueError, on the client or on a passed httpx2_client. With a cap set, httpware follows the redirects itself and caps only the final response. Intermediate redirect responses are closed without reading their bodies, so the responses in response.history have no content. Client auth is sent on the first hop only; httpx2's own rules decide whether the Authorization header carries over to later hops. Multi-step auth such as DigestAuth does not answer a challenge that arrives after a redirect. Without a cap nothing changes.
Full Changelog: 0.19.0...0.20.0
0.19.0
What's Changed
- docs: point event_hooks at a caller-built httpx2 client by @lesnik512 in #149
- feat: make type checkers reject unknown client options by @lesnik512 in #150
- docs: fix stale facts and plain up user-facing prose by @lesnik512 in #151
New dependency
httpware now depends on typing-extensions>=4.14.1 on every Python version. Before this, httpx2 was its only runtime dependency. It's needed for the closed TypedDicts that let ty reject unknown client options such as verfy= or cert= (pyright and mypy already did). The floor matters on Python 3.14: earlier releases of typing-extensions report no keys for these TypedDicts there, which would make the client reject every option.
Full Changelog: 0.18.0...0.19.0
0.18.0
What's Changed
- feat: forward every httpx2 client option to the owned client by @lesnik512 in #148
Behaviour change
AsyncClient/Client now raise ValueError when max_response_body_bytes is set and the client follows redirects. This applies both to follow_redirects=True and to a caller-provided httpx2_client built with follow_redirects=True, which was accepted before. httpx2 reads intermediate redirect bodies without the cap, so the cap did not protect that path. Either drop follow_redirects or drop the cap.
Full Changelog: 0.17.2...0.18.0
0.17.2
Behaviour change: constructing Client or AsyncClient with a base_url that contains a query string now raises ValueError, including via httpx2_client=. httpx2 was producing malformed URLs from it (the request path ended up inside the query). Put shared query parameters in params= instead.
What's Changed
- fix: reject a base_url that contains a query string by @lesnik512 in #147
Full Changelog: 0.17.1...0.17.2
0.17.1
Behaviour change: when a URL already has a query string, params (per-request or client-level) are now merged into it instead of replacing it. A key passed in params overrides the same key in the URL. This works around pydantic/httpx2#905.
What's Changed
- ci: fetch the ADR citation check from .github instead of keeping a copy by @lesnik512 in #144
- fix: keep the URL's query string when params are passed by @lesnik512 in #145
Full Changelog: 0.17.0...0.17.1
0.17.0
What's Changed
- ci: run the scheduled dependency check daily by @lesnik512 in #131
- fix(deps): mark the msgspec and pydantic floors by @lesnik512 in #133
- ci: resolve the declared dependency floors on every matrix entry by @lesnik512 in #134
- ci: skip the floors job on scheduled runs by @lesnik512 in #135
- ci: pin the declared floors before installing them wheel-only by @lesnik512 in #137
- chore: align with the org standard (TS1) by @lesnik512 in #136
- ci: test 3.14t in the pytest matrix instead of a separate job by @lesnik512 in #138
- chore: make keywords mirror the GitHub topics (MD3) by @lesnik512 in #139
- chore: give every coverage pragma a reason (TS6, TS5) by @lesnik512 in #140
- refactor: delete the unreachable None branch in retry-after parsing by @lesnik512 in #141
- fix: stop Retry-After HTTP dates without a zone or out of range from crashing the retry by @lesnik512 in #143
- feat: add AsyncKeyedCircuitBreaker and KeyedCircuitBreaker by @lesnik512 in #142
Full Changelog: 0.16.1...0.17.0
0.16.1
What's Changed
- docs: interactive resilience demo pages by @lesnik512 in #109
- docs: make retry demo failure count obvious by @lesnik512 in #110
- docs: thundering-herd demo view + resilience-demo enrich pass by @lesnik512 in #111
- docs: drop redundant sustained scenario from single-client retry demo by @lesnik512 in #112
- chore: adopt ruff 0.16.0 by @lesnik512 in #114
- chore: rename CLAUDE.md to AGENTS.md and Justfile to justfile by @lesnik512 in #115
- fix: restore CLAUDE.md importing AGENTS.md by @lesnik512 in #116
- chore: migrate off the planning/ convention by @lesnik512 in #119
- chore(ci): check absolute self-links in the offline link gate by @lesnik512 in #120
- docs(agents): drop the fact-placement convention from AGENTS.md by @lesnik512 in #121
- docs: write README links absolute so they resolve on PyPI by @lesnik512 in #122
- chore(ruff): converge on the standard's ruff block by @lesnik512 in #123
- chore(coverage): declare the gate in [tool.coverage.report] and measure only in test-ci by @lesnik512 in #124
- docs(agents): drop the retired paragraphs and keep the canonical ones verbatim by @lesnik512 in #125
- chore(docs): use a builder-portable homepage check in the override template by @lesnik512 in #126
- test: fail when an ADR cited from Python does not resolve by @lesnik512 in #127
- docs(adr): compress to 10 records in the domain-modeling format by @lesnik512 in #128
- test: report a bare docs/adr/NNNN citation as unresolved by @lesnik512 in #129
- docs: restore the agent artifacts to the setup skill's templates by @lesnik512 in #130
Full Changelog: 0.16.0...0.16.1
0.16.0
httpware 0.16.0 — free-threading (nogil) support, Beta
Certifies httpware under free-threaded CPython (PEP 703), backed by CI
evidence rather than a bare classifier.
Feature
- Free-threading support (Beta). Adds the
Programming Language :: Python :: Free Threading :: 2 - Betaclassifier.
A newpytest-freethreadedCI job runs the full test suite (all extras) on
free-threaded CPython3.14twith the GIL disabled. Real-thread stress tests
(markedpytest.mark.stress) exercise the thread-shared components — the sync
Bulkhead,CircuitBreaker, and the sharedRetryBudget— plus thehttpx2
connection pool, with zero cross-talk or crashes under parallelism; a separate
deterministic test covers the single-event-loop guard's cross-loop rejection.3.13tis deferred until msgspec ships a cp313t
wheel (planning/deferred.md).
Why
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.
Downstream
No API changes. Safe to upgrade unconditionally; the classifier and CI job
are the only visible additions.
Internals
- New
stresspytest marker (architecture/testing.md): runs under the GIL
too (counts toward coverage) but only proves thread-safety on 3.14t. architecture/resilience.md,architecture/overview.mdupdated with the
free-threading promotion.