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
{{ message }}
Repository navigation
MockTransport doesn't set the elapsed property #3712
This issue already has two PRs with proposed fixes, but for now I suggest a very simple way to fix it without extra boilerplate code. We can close the PRs until we make a good design decision around this.
You can write a very simple wrapper around that mock transport and use it instead of the default one. Then you can handle the elapsed time for mocked responses however you want, like this.
from __future__ importannotationsfromdatetimeimporttimedeltaimporthttpxclassCustomMock(httpx.BaseTransport):
def__init__(self, next_transport: httpx.BaseTransport|None=None) ->None:
self._next_transport=httpx.MockTransport() ifnext_transportisNoneelsenext_transportdefhandle_request(self, request: httpx.Request) ->httpx.Response:
response=self._next_transport.handle_request(request)
response.elapsed=timedelta()
returnresponse# use custom mock instead of the default mock
The same gap is not limited to MockTransport — it hits any transport that hands back a Response whose body is already loaded:
importhttpxclassPreloaded(httpx.BaseTransport):
defhandle_request(self, request):
returnhttpx.Response(200, content=b"ok", request=request)
withhttpx.Client(transport=Preloaded()) asclient:
r=client.get("http://example.org/")
print(r.is_closed, r.is_stream_consumed) # True Trueprint(r.elapsed)
# RuntimeError: '.elapsed' may only be accessed after the response has been read or closed.
Same with MockTransport + client.stream(...) when the handler returns a response built with content=, so #3757 does not cover the general case either.
Root cause on master (0.28.1, httpx/_client.py:139-159): BoundSyncStream.close() is the only place that sets _elapsed, and it never runs for a preloaded body. Response.__init__ sets _content, so Response.read() (httpx/_models.py:876) short-circuits and no byte iterator, hence no close(), is ever created:
So a response that is fully buffered from the start is the one case where is_closed/is_stream_consumed become True without _elapsed existing — the two flags the error message points at are both already True, which makes the message misleading.
Two other spots feed the same complaint:
httpx/_transports/mock.py:28,45 returns the handler's response unmodified, so a handler that does set resp.elapsed = timedelta(seconds=2.5) has no effect once the client wraps it — the value you set is silently replaced by the transport round-trip time (measured 0:00:00).
nothing sets _elapsed when the transport raises or when send() returns before reading, which is the 0 case people work around manually.
Given the design discussion in #3712, the minimal consistent rule would be: set _elapsed at send() return time for responses that are already buffered, and keep BoundSyncStream.close() for streamed ones. That makes .elapsed available for every closed response without asking handlers to compute timings.
Activity
fix: set default elapsed on MockTransport responses
karpetrosyan commentedon Dec 10, 2025
This issue already has two PRs with proposed fixes, but for now I suggest a very simple way to fix it without extra boilerplate code. We can close the PRs until we make a good design decision around this.
You can write a very simple wrapper around that mock transport and use it instead of the default one. Then you can handle the elapsed time for mocked responses however you want, like this.
Add test for MockTransport elapsed property and implement fix
Fix MockTransport not setting elapsed property
ege-arhan commentedon Oct 23, 2026
The same gap is not limited to
MockTransport— it hits any transport that hands back aResponsewhose body is already loaded:Same with
MockTransport+client.stream(...)when the handler returns a response built withcontent=, so #3757 does not cover the general case either.Root cause on master (
0.28.1,httpx/_client.py:139-159):BoundSyncStream.close()is the only place that sets_elapsed, and it never runs for a preloaded body.Response.__init__sets_content, soResponse.read()(httpx/_models.py:876) short-circuits and no byte iterator, hence noclose(), is ever created:So a response that is fully buffered from the start is the one case where
is_closed/is_stream_consumedbecomeTruewithout_elapsedexisting — the two flags the error message points at are both alreadyTrue, which makes the message misleading.Two other spots feed the same complaint:
httpx/_transports/mock.py:28,45returns the handler's response unmodified, so a handler that does setresp.elapsed = timedelta(seconds=2.5)has no effect once the client wraps it — the value you set is silently replaced by the transport round-trip time (measured0:00:00)._elapsedwhen the transport raises or whensend()returns before reading, which is the0case people work around manually.Given the design discussion in #3712, the minimal consistent rule would be: set
_elapsedatsend()return time for responses that are already buffered, and keepBoundSyncStream.close()for streamed ones. That makes.elapsedavailable for every closed response without asking handlers to compute timings.