Summary
http_request_timeout does not bound an ably.request() (or any other REST call). A
request can run far past it, and in one measured case ran for 20 seconds against a
15-second retry budget.
Found while tracing an intermittent CI timeout in TestRestRequest, where a setup fixture
hung inside ssl.read on an HTTP/2 stream read. The test-side exposure was reduced
separately (#705); this is the underlying library behaviour.
Three separate reasons a request is unbounded
1. The read timeout applies per socket read, not per response.
Http.make_request passes the option through as the httpx read timeout:
timeout = (self.http_open_timeout, self.http_request_timeout)
httpx expands a 2-tuple to {'connect': ..., 'read': ..., 'write': None, 'pool': None},
and httpcore applies read to a single socket.recv(). _receive_stream_event then
loops until the stream event it wants arrives, taking a fresh timeout each iteration — so
any frame on the connection restarts the clock. A response that never completes, but on a
connection that keeps producing frames, is never timed out.
2. write and pool resolve to None. A body write on a wedged connection blocks
indefinitely, and so does waiting for a pool slot.
3. The retry budget is only consulted after an attempt has already failed.
should_stop_retrying is evaluated in the exception and fallback paths:
def should_stop_retrying(retry_count=retry_count):
time_passed = time.time() - requested_at
return retry_count == len(hosts) - 1 or time_passed > http_max_retry_duration
http_max_retry_duration (15s by default) therefore caps nothing while an attempt is still
in flight. A read timeout that does fire retries onto a fallback host, and the elapsed
check only then notices the budget is spent.
Measurement
Against a local blackhole server that accepts connections instantly and never responds:
20.0 seconds for a single request(), with http_request_timeout at its default and
http_max_retry_duration at 15s.
Suggested direction
Bound the whole request rather than an individual socket read — e.g. wrap the attempt in
an overall deadline, and give write and pool real values. Note this changes RSC15
fallback semantics (how much of the budget one attempt may consume), so it wants a spec
conversation rather than a drop-in patch.
🤖 Generated with Claude Code
┆Issue is synchronized with this Jira Task by Unito
Summary
http_request_timeoutdoes not bound anably.request()(or any other REST call). Arequest can run far past it, and in one measured case ran for 20 seconds against a
15-second retry budget.
Found while tracing an intermittent CI timeout in
TestRestRequest, where a setup fixturehung inside
ssl.readon an HTTP/2 stream read. The test-side exposure was reducedseparately (#705); this is the underlying library behaviour.
Three separate reasons a request is unbounded
1. The read timeout applies per socket read, not per response.
Http.make_requestpasses the option through as the httpx read timeout:httpx expands a 2-tuple to
{'connect': ..., 'read': ..., 'write': None, 'pool': None},and httpcore applies
readto a singlesocket.recv()._receive_stream_eventthenloops until the stream event it wants arrives, taking a fresh timeout each iteration — so
any frame on the connection restarts the clock. A response that never completes, but on a
connection that keeps producing frames, is never timed out.
2.
writeandpoolresolve toNone. A body write on a wedged connection blocksindefinitely, and so does waiting for a pool slot.
3. The retry budget is only consulted after an attempt has already failed.
should_stop_retryingis evaluated in the exception and fallback paths:http_max_retry_duration(15s by default) therefore caps nothing while an attempt is stillin flight. A read timeout that does fire retries onto a fallback host, and the elapsed
check only then notices the budget is spent.
Measurement
Against a local blackhole server that accepts connections instantly and never responds:
20.0 seconds for a single
request(), withhttp_request_timeoutat its default andhttp_max_retry_durationat 15s.Suggested direction
Bound the whole request rather than an individual socket read — e.g. wrap the attempt in
an overall deadline, and give
writeandpoolreal values. Note this changes RSC15fallback semantics (how much of the budget one attempt may consume), so it wants a spec
conversation rather than a drop-in patch.
🤖 Generated with Claude Code
┆Issue is synchronized with this Jira Task by Unito