Skip to content

Releases: modern-python/httpware

0.22.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 12:38
0.22.0
a7cfa3a

What's Changed

  • fix!: errors and redaction (pickling, query masking, DecodeError message, read-only STATUS_TO_EXCEPTION) by @lesnik512 in #166
  • fix: decoder cache keys, msgspec field claims, unread middleware responses by @lesnik512 in #168
  • fix: circuit breaker ignores straggler outcomes, unwraps budget exhaustion, rate-limits circuit.rejected by @lesnik512 in #167
  • fix!: consistent client error paths (timeout=None, RemoteProtocolError, invalid URL, closed client, streaming bodies, async middleware) by @lesnik512 in #169
  • fix!: validate retry and budget config, case-insensitive retry_methods, Retry-After give-up event by @lesnik512 in #177
  • docs: put the circuit breaker before AsyncTimeout; document rate-mode and event-loop behaviour by @lesnik512 in #178
  • fix: capped responses keep elapsed, next_request and default_encoding; capped redirect and auth hops reuse their connection by @lesnik512 in #179
  • perf: build the StatusError message on first use instead of at construction by @lesnik512 in #180
  • perf: count RetryBudget and the rate-mode window in time buckets by @lesnik512 in #181
  • perf: cut per-request overhead in the client hot path by @lesnik512 in #182
  • perf: cheaper hot paths in the circuit breakers, AsyncBulkhead and the event-loop guard by @lesnik512 in #183
  • refactor: declare per-request options once as TypedDicts in client.py by @lesnik512 in #186
  • test: synchronize bulkhead capacity tests on events instead of sleeps by @lesnik512 in #185
  • test: synchronize stream cancellation test on an event instead of a sleep by @lesnik512 in #188
  • fix: map InvalidURL in build_request; clear TypeError for sync-stream middleware responses by @lesnik512 in #189
  • fix: drop None attributes from OTel span events by @lesnik512 in #190
  • fix: bound the decoder adapter caches by @lesnik512 in #191
  • perf: count capped bytes on the stream for unencoded bodies by @lesnik512 in #192
  • (and the CI, docs-demo and tooling PRs #158 to #165; GitHub's "Generate release notes" lists them)

Breaking changes

timeout=None means no timeout. Client(timeout=None) and AsyncClient(timeout=None) used to be treated as "not set" and kept httpx2's 5 second default. They now disable the timeout, as in httpx2. To keep the default, leave timeout out. Passing timeout=None together with httpx2_client= now raises the same TypeError as any other option combined with a client you built yourself. (#169)

STATUS_TO_EXCEPTION is read-only. It is now a MappingProxyType, so assigning to it raises TypeError. To raise your own class for a status, catch the httpware error in a middleware (an on_error handler, for example) and raise yours from it. (#166)

Retry and budget config is validated at construction. Retry, AsyncRetry and RetryBudget raise ValueError for values that used to crash or misbehave at request time:

  • base_delay must be >= 0 (inf is still accepted).
  • max_delay must be finite and >= 0. Replace max_delay=float("inf") with a large finite number.
  • RetryBudget.ttl must be finite and > 0.
  • min_retries_per_sec and percent_can_retry must be finite and >= 0. To switch the budget off in practice, pass a large min_retries_per_sec.

NaN is rejected everywhere. If construction now fails, the old value was already broken once a retry happened. (#177)

Behaviour changes

Errors and observability:

  • Exception messages, reprs, log records and span events show every query value as REDACTED, keeping the keys. Log queries or alerts that matched on query values need updating. The raw URL is still on exc.request.url. (#166)
  • str(DecodeError) reads failed to decode response into <model>: <ErrorType> (n errors) and no longer includes the decoder's message, which could echo body content. The original error is on exc.original. (#166)
  • Notes and attributes added to any httpware exception survive pickle and copy. (#166)
  • httpx2.RemoteProtocolError ("server disconnected") now raises NetworkError, so Retry retries it and circuit breakers count it. (#169)
  • An invalid URL given to a verb method or request() raises TransportError instead of httpx2.InvalidURL. stream() on a closed client raises TransportError instead of RuntimeError. (#169)
  • Client(middleware=[...]) raises TypeError for async middleware instead of silently sending nothing. (#169)
  • build_request maps an invalid URL to TransportError too, like every other method. (#189)
  • A middleware response whose body iterator is the wrong kind for the client (a sync iterator on AsyncClient, an async one on Client) raises TypeError when decoded. Before, AsyncClient raised a bare RuntimeError from httpx2 and Client raised a DecodeError that blamed the decoder. (#189)
  • OpenTelemetry span events leave out attributes whose value is None (such as last_status when the last attempt raised), so SDKs before 1.45 no longer log a warning on each give-up. On SDK 1.45+, the key is now missing instead of None. Log records keep the None values. (#190)
  • The RuntimeError for an async bulkhead or breaker used from a second event loop has a new message that says to create them once per loop. Code matching the old text needs updating. (#178)

Retry:

  • retry_methods matches regardless of case, so {"post"} now retries POST. (#177)
  • Giving up because Retry-After exceeds max_delay emits retry.giving_up. Every retry.giving_up event now has a reason attribute: max_attempts or retry_after_exceeds_max_delay. (#177)
  • A Retry-After too large for a float makes the retry give up instead of retrying immediately. (#177)
  • Requests passed to send() or send_with_response() with a generator, iterator or file body are no longer retried, as with the verb methods. (#169)
  • RetryBudget counts in steps of ttl / 10: a deposit or withdrawal stops counting between 0.9 * ttl and ttl after it was made. With the default ttl=10.0, a request can leave the budget up to a second early. Memory no longer grows with traffic. (#181)

Circuit breaker:

  • A request that was in flight when its circuit changed state no longer affects the circuit. Before, a late result could reopen a half-open circuit, close it without a successful probe, or let two probes run at once. (#167)
  • A RetryBudgetExhaustedError counts against the breaker according to its last_exception, so a breaker outside a starved retry now opens on repeated 503s. (#167)
  • circuit.rejected fires at most once per second per circuit, and always for the first rejection of an outage, with a new suppressed count. circuit.closed has a new rejected attribute with the outage's total. To count rejections from logs, sum 1 + suppressed or read rejected. (#167)
  • Breaker events are emitted after the sync lock is released, so a logging handler may send requests through the same breaker without deadlocking. (#167)
  • The key function of KeyedCircuitBreaker runs outside the breaker lock, so several threads can call it at once. Pure functions of the request are unaffected. (#183)

Recommended middleware order changed. The docs now recommend AsyncCircuitBreaker → AsyncTimeout → AsyncBulkhead → AsyncRetry. In the old order, with AsyncTimeout first, the breaker saw an expired deadline as a cancellation and never counted it, so a hanging upstream never opened the circuit. Existing chains keep working; swap the first two entries to get timeouts counted. (#178)

Decoders:

  • MsgspecDecoder no longer claims a Struct, dataclass, TypedDict or NamedTuple with a field msgspec cannot decode. Those requests now fail with MissingDecoderError before sending, instead of sending and then failing with DecodeError. With the default decoder list, such dataclasses and TypedDicts still go to PydanticDecoder. (#168)
  • Each decoder caches parsers for up to 4,096 models; the next new model after that empties the cache. Models created per request no longer grow memory without bound, but they get their parser rebuilt after each clear, so define response models once. (#191)
  • A | B and B | A no longer share a cached adapter, so pydantic's member-order tie-break applies per call site. (#168)
  • A streamed response returned by middleware without being read now decodes, in both clients. (#168, #169)

Response body cap (max_response_body_bytes):

  • A capped response keeps elapsed, next_request and the client's default_encoding, matching an uncapped one. (#179)
  • A capped response without Content-Encoding is now the response httpx2 built, read through a byte-counting stream, instead of a rebuilt copy. It reports the real num_bytes_downloaded instead of 0 and keeps its Transfer-Encoding header and network_stream extension. Encoded responses are still rebuilt and still report 0. (#192)
  • Redirect and auth hop bodies are read up to the cap and thrown away, so hops reuse the pooled connection. A five-hop redirect chain used to open six connections. A hop body larger than the cap is still not an error. (#179)

Performance

  • A capped request with an unencoded body costs about 13 to 15 µs (2...
Read more

0.21.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 08:35
0.21.0
5d94ba2

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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 09:53
0.20.1
afeaf59

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:

  • DigestAuth answers a challenge that arrives after a redirect (0.20.0 could not).
  • Credentials written into a redirect Location URL 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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 09:23
0.20.0
67d742c

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

Choose a tag to compare

@github-actions github-actions released this 01 Oct 20:18
0.19.0
05a4319

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

Choose a tag to compare

@github-actions github-actions released this 01 Oct 11:16
0.18.0
838b95c

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

Choose a tag to compare

@github-actions github-actions released this 28 Sep 20:59
0.17.2
05056f5

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

Choose a tag to compare

@github-actions github-actions released this 28 Sep 20:26
0.17.1
8878a3d

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

Choose a tag to compare

@github-actions github-actions released this 27 Sep 17:33
0.17.0
d97ea96

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

Choose a tag to compare

@github-actions github-actions released this 19 Sep 11:08
0.16.1
72fe4ba

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