FAQs¶
On this page are some examples of usage and commonly asked questions.
Chaining transports¶
HTTPX Retries is implemented as a custom Transport. It's common to want to add additional custom behaviour (eg. rate-limiting, proxies and more), and it's possible to chain transports to layer in behaviours; the first argument to the RetryTransport constructor is a transport to wrap.
with AsyncClient(
transport=RetryTransport(
RateLimitTransport(
AsyncHTTPTransport(),
interval=timedelta(seconds=1),
count=3,
),
retry=Retry(total=5, backoff_factor=0.5),
),
timeout=90.0,
) as client:
...
Client timeout or Retry timeout?¶
Does the httpx.Client timeout apply to each request made in a series of retries, or to the "user request", who wants
the result in the next 10s, regardless of how many times the underlying HTTP client needs to try to get it?
The timeout passed to httpx.Client applies to each request in a series of retries. If a httpx.TimeoutException is
raised and a retry is applicable, the same request, with the same timeout values, will be retried.
In the case where retries are bounded by individual (client) timeouts, it is always possible to work out the maximum possible time taken to execute all retries.
So if you wanted to add a lower overall retry value than the number of retries multiplied by the client timeout, you could tweak the max_backoff_wait and backoff_factor. Eg.
would result in a retry-inclusive timeout of 15s + 3 * (client_timeout).
Why wasn't my ReadTimeout retried?¶
If an error like httpx.ReadTimeout or httpx.RemoteProtocolError("peer closed connection without sending complete message body") escapes your client with no retry attempts logged, this is expected given HTTPX's transport architecture, not a bug in RetryTransport.
HTTPX transports return as soon as response headers are received. The response body is read lazily inside the client — by response.read(), response.aread(), or iteration over a streaming response. Errors that occur while reading the body are raised directly to the caller, bypassing the transport. Since RetryTransport only observes what flows through its handle_request / handle_async_request methods, it cannot retry these body-phase errors.
Retried by RetryTransport:
httpx.TimeoutException,httpx.NetworkError, andhttpx.RemoteProtocolErrorraised before response headers arrive (for example, a connect timeout, or a slow server that doesn't send headers in time).- Responses with a retryable status code (default:
429,502,503,504).
Not retried by RetryTransport:
- Any exception raised during
response.read(),response.aread(), or iteration of a streaming response — includingReadTimeoutmid-body andRemoteProtocolError("peer closed connection...").
Most requests don't need this! Keep using RetryTransport
A body-phase error can only occur while the body is in transit, so for the small responses typical of most API traffic that window is tiny and almost every failure is already caught by RetryTransport at the header or status-code stage. The helpers below earn their keep in a narrower set of cases: reading large or slow, non-streamed bodies — big exports and downloads, or payloads proxied from a flaky origin — where a mid-body ReadTimeout or a truncated RemoteProtocolError is more likely. For ordinary requests, prefer RetryTransport for simplicity.
To retry body-phase errors, use retry_request (or its async counterpart aretry_request). These helpers drive the retry loop at the client level, where the body is read, so the same Retry configuration covers body-phase errors as well as the header-phase errors and retryable status codes that RetryTransport already handles:
import httpx
from httpx_retries import Retry, retry_request
with httpx.Client() as client:
response = retry_request(client, "GET", "https://example.com", retry=Retry(total=5, backoff_factor=0.5))
import httpx
from httpx_retries import aretry_request
async with httpx.AsyncClient() as client:
response = await aretry_request(client, "GET", "https://example.com")
A plain client is all you need — the helpers run the full retry loop themselves, so there's no need to also install RetryTransport. Passing a client that uses RetryTransport raises a ValueError, because it would retry every request twice.
These helpers buffer the full response body
Because the body is read before the helper returns, retry_request and aretry_request are not suitable for streaming. An error raised while iterating a streaming response (client.stream(...)) happens after the body has started arriving and cannot be retried transparently — bytes already handed to your code can't be recalled. For streaming, catch the error and re-issue the request yourself.
Only idempotent methods are retried by default
Like RetryTransport, the helpers only retry methods in Retry.allowed_methods (by default HEAD, GET, PUT, DELETE, OPTIONS, TRACE); other methods are sent once. You can opt a method in with Retry(allowed_methods=[...]), but take care: a body-phase retry re-issues the entire request, and because the server has already started responding it has most likely processed the original — so only enable non-idempotent methods such as POST when duplicate side effects are acceptable.
Retrying on response content¶
Sometimes a server returns a valid response but the body or custom headers signals a failure - for example, a block page, a CAPTCHA redirect, or an authorization wall. This commonly occurs if access may be blocked at the content level rather than the HTTP status level.
Use validate_response to inspect the response and raise an exception to trigger a retry:
import httpx
from httpx_retries import Retry, RetryTransport
class ContentBlocked(ValueError):
pass
def validate_response(response: httpx.Response) -> None:
# safely inspect status and headers if needed
response.raise_for_status()
# NOTE: Do not call `.read()` here with `Client.stream`,
# it will buffer the entire body, which defeats the purpose of streaming.
response.read()
if "content blocked" in response.text:
raise ContentBlocked(response.text)
retry = Retry(validate_response=validate_response, retry_on_exceptions=[httpx.HTTPStatusError, ContentBlocked])
with httpx.Client(transport=RetryTransport(retry=retry)) as client:
response = client.get("https://example.com")
Do not call response.read() inside validate_response with Client.stream
validate_response is called before the response is returned to the caller. Calling response.read() or await response.aread() inside it will buffer the entire body, which defeats the purpose of streaming. If you use streaming, validate only the status code and headers.
Limits / Cert / SSL / http2 parameters passed to the client are not being applied¶
This is a limitation of the way transports are applied to clients in HTTPX. If you provide a custom transport, several parameters
that can be passed to httpx.Client are ignored. The workaround is to directly provide an instance of
httpx.HTTPTransport (or the async variant) which
will accept these parameters.
with Client(
transport=RetryTransport(
HTTPTransport(
# Pass a transport with these parameters to wrap.
http2=True,
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100, keepalive_expiry=60),
verify=False
),
retry=Retry(total=5, backoff_factor=0.5),
)
# These will do nothing!
http2=True,
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100, keepalive_expiry=60),
verify=False
) as client:
...