You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix retry-budget exhaustion and correlated reused-connection death (#1354)
* Fix retry-budget exhaustion and correlated reused-connection death
A client sending periodic bursts against a peer that silently discards
idle pooled connections saw transient errors escape HTTP.request with
default retry=true after ~50 burst cycles, permanently (#1353). Root
cause: every armed high-level retry consumed 10 of the per-host
RetryBucket's 500 tokens even when the retried attempt succeeded, the
bucket never refilled, and once empty the denial was silent — so the
budget was a fuse that burned out in ~50 recoveries, after which every
transient failure surfaced raw.
- RetryBucket: refund the reservation when a retried attempt reaches a
non-retryable response (retryable 408/429/5xx responses keep partial
cost, exception outcomes keep full cost), and credit 1 unit per
successful non-retried request so a drained partition heals from
healthy traffic. A depleted-partition counter keeps the per-request
replenish check lock-free while all partitions are full.
- Transport: retry a replayable idempotent request while failures keep
landing on *reused* pooled connections (bounded by max_idle_per_host
+ 1 acquisitions) instead of exactly once, so a correlated-death
batch is burned through down to a fresh dial without consuming
high-level retry budget; classify TLSError on reused connections by
its cause so dead reused TLS connections take this path too.
- Add RetrySkippedEvent so a denied (:retry_bucket) or deadline-
preempted (:deadline) retry is observable in request traces.
- Add TLSTransportError for TLS I/O failures on established
connections; handshake failures are now typed TLSHandshakeError at
the dial sites instead of one blanket wrap that mislabeled
mid-request read errors as handshake errors. isrecoverable unwraps
both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Cover TLS dial-site wrapping and RetrySkippedEvent emission paths
Add tests for the handshake-phase TLSHandshakeError wrap on both the
HTTP/1 transport dial and connect_h2!, the request-path (exception)
RetrySkippedEvent emission, and the verbose trace formatting of
RetrySkippedEvent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(client): harden retry and transport recovery
Make retry-budget accounting exact across built-in and custom
policies. Recover poisoned pooled connections without leaking slots or
reusing a failed connection. Normalize established TLS failures across
HTTP/1 and HTTP/2 public client boundaries.
Add regression coverage for terminal accounting, trace failures,
one-shot bodies, concurrent pool handoff, and truncated TLS records.
Fixes#1353
* Centralize the TLS truncation-message classification
Both retry classifiers matched Reseau's truncated-TLS-stream message with
a duplicated string literal; hoist it to a single documented constant so
a Reseau wording change is a one-line fix, and note that the end-to-end
truncation tests pin the coupling. Also smooth the retry-budget
CHANGELOG entry into readable prose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+40Lines changed: 40 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,12 +10,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
### Added
11
11
- Added `HTTP2Settings` to configure HTTP/2 receive flow-control windows (per-stream `initial_window_size` and connection-level `connection_window_size`). Pass it via the `http2_settings` keyword on `Client`, `Server`, `listen!`, `serve!`, `serve`, and `connect_h2!`. Defaults preserve the protocol-default 65535-byte windows, and the per-stream receive buffer cap is derived from the window. Raising the windows improves single-stream throughput on links with non-trivial latency.
12
12
- Added `HTTP.peeraddr(::HTTP.Stream)`, returning the remote (client) `SocketAddr` of a server stream for both plain-TCP and TLS connections and both HTTP/1 and HTTP/2. This is the supported way to obtain the client IP (for rate limiting, audit logging, and per-client policy) without reaching into transport internals, and restores the capability `Sockets.getpeername(::HTTP.Stream)` provided in HTTP.jl 1.x.
13
+
- Added `HTTP.RetrySkippedEvent`, a request trace event emitted when the retry
14
+
policy wanted to retry an attempt but the retry was not armed — because the
15
+
transport's `RetryBucket` denied capacity (`reason = :retry_bucket`) or the
16
+
request deadline preempted the backoff (`reason = :deadline`). Previously a
17
+
denied retry was indistinguishable from a non-retryable failure. ([#1353])
18
+
- Added `HTTP.TLSTransportError`, raised when TLS-level I/O fails on an
19
+
established connection during a request. Previously such failures were
20
+
mislabeled `TLSHandshakeError`; that type is now reserved for actual
21
+
connection-setup failures. `HTTP.isrecoverable` classifies both wrappers by
22
+
their underlying cause. ([#1353])
13
23
14
24
### Fixed
15
25
- Restored HTTP and WebSocket server task scheduling to Julia's `:interactive`
16
26
thread pool so default-pool compute work cannot starve server and health-check
17
27
tasks when an interactive thread is configured. ([#1342])
18
28
- Percent-decode `userinfo` before building the `Basic` auth header (RFC 3986); fixes wrong credentials for request URLs and proxies containing percent-encoded characters.
29
+
- Fixed the client retry budget (`RetryBucket`) treating a successful retried
30
+
attempt as a full-cost failure. The per-host budget drained by 10 of 500
31
+
units on every retry — even one that recovered with a 2xx — and never
32
+
refilled, so after ~50 retries against a host every subsequent retry was
33
+
silently denied for the transport's lifetime and transient errors surfaced
34
+
raw despite `retry=true`. A retry reservation is now settled by the
35
+
effective retry decision for the response it produced: refunded in full when
36
+
the built-in policy (or a custom `retry_if`) no longer wants a retry, and
37
+
keeping the partial cost when the response is still classified as a failure.
38
+
On the final attempt the built-in classification applies without invoking
39
+
`retry_if`, and a retry that `retry_if` explicitly requested conservatively
40
+
keeps cost on a non-2xx/3xx outcome. Each successful non-retried request
41
+
restores one unit of previously consumed budget, retry reservations and
42
+
response connections are released even when a trace or retry-policy callback
43
+
throws, and the request deadline is rechecked after the backoff sleep.
44
+
([#1353])
45
+
- The HTTP/1 transport now retries a replayable idempotent request for as long
46
+
as failures land on *reused* pooled connections. It tries at most
47
+
`max_idle_per_host` reused connections, then forces a fresh dial instead of
48
+
accepting another concurrent pool return. PUT and DELETE receive the same
49
+
stale-connection recovery as the other idempotent methods.
50
+
Pooled connections can be discarded by the peer in correlated batches, in
51
+
which case the single retry would draw the next equally-dead pooled
52
+
connection and fail. ([#1353])
53
+
- Dead reused TLS connections that fail with `Reseau.TLS.TLSError` (for
54
+
example an RST surfacing as a wrapped `SystemError` or a truncated TLS record
55
+
reported as `unexpected EOF`) are now classified by their public error shape
56
+
in the transport's reused-connection retry. HTTP/2 read-loop wrappers also
57
+
preserve this classification. ([#1353])
19
58
20
59
## [v2.0.0] - 2026-04-27
21
60
HTTP.jl 2.0 is a major rewrite of the package internals and public API. The
@@ -826,3 +865,4 @@ See changes for 0.9.15: this release is equivalent to 0.9.15 with [#752] reverte
0 commit comments