respx mocks httpx2 at the transport layer via using="httpcore2", and pytest-httpx2 ships that mocker. When a test hands a response object to a route, respx still requires httpx.Response and rejects httpx2.Response, even though the request runs through httpx2.
Reproduce
import httpx2
import respx
@respx.mock(using="httpcore2")
def test_retry():
respx.get("https://example.com").mock(
side_effect=[
httpx2.ReadTimeout("timed out"),
httpx2.Response(200, text="ok"), # rejected
]
)
httpx2.get("https://example.com")
This fails at runtime, not only under a type checker.
Cause (respx 0.23.1)
Three guards hard-code httpx.Response:
respx/models.py:199: the return_value setter raises TypeError(f"{return_value!r} is not an instance of httpx.Response").
respx/router.py:265, 314, 319: the resolver and handlers assert isinstance(resolved.response, httpx.Response).
A side_effect element holding an httpx2.Response trips the assert. A return_value trips the setter.
Why loosening the guards looks cheap
Everything downstream reads the response structurally, and httpx2.Response exposes the same attributes:
clone_response() (models.py:34) reads .status_code, .headers, .stream, .extensions.
- the httpcore conversion (
mocks.py:322) reads .status_code, .headers.raw, .stream.
An httpx2.Response would pass through as a carrier of status, headers, and stream, the same role httpx.Response fills today. Accepting either type at the guards (or a structural check, to skip importing httpx2) covers the minimal case. respx can keep constructing httpx.Response internally; the caller-supplied object only needs to clear the guards.
What surprised me
Mocking httpx2 but feeding it httpx.Response reads backwards, and importing the legacy package in an httpx2-only test suite stands out. Neither the respx nor pytest-httpx2 docs mention the requirement.
Workaround for anyone hitting this
Return httpx.Response (or respx.MockResponse) for the response and keep httpx2.* for raised exceptions:
import httpx
respx.get(...).mock(side_effect=[httpx2.ReadTimeout("timed out"), httpx.Response(200, text="ok")])
I can send a PR for the minimal guard change if it would be welcome.
respx mocks httpx2 at the transport layer via
using="httpcore2", and pytest-httpx2 ships that mocker. When a test hands a response object to a route, respx still requireshttpx.Responseand rejectshttpx2.Response, even though the request runs through httpx2.Reproduce
This fails at runtime, not only under a type checker.
Cause (respx 0.23.1)
Three guards hard-code
httpx.Response:respx/models.py:199: thereturn_valuesetter raisesTypeError(f"{return_value!r} is not an instance of httpx.Response").respx/router.py:265, 314, 319: the resolver and handlers assertisinstance(resolved.response, httpx.Response).A
side_effectelement holding anhttpx2.Responsetrips the assert. Areturn_valuetrips the setter.Why loosening the guards looks cheap
Everything downstream reads the response structurally, and
httpx2.Responseexposes the same attributes:clone_response()(models.py:34) reads.status_code,.headers,.stream,.extensions.mocks.py:322) reads.status_code,.headers.raw,.stream.An
httpx2.Responsewould pass through as a carrier of status, headers, and stream, the same rolehttpx.Responsefills today. Accepting either type at the guards (or a structural check, to skip importing httpx2) covers the minimal case. respx can keep constructinghttpx.Responseinternally; the caller-supplied object only needs to clear the guards.What surprised me
Mocking httpx2 but feeding it
httpx.Responsereads backwards, and importing the legacy package in an httpx2-only test suite stands out. Neither the respx nor pytest-httpx2 docs mention the requirement.Workaround for anyone hitting this
Return
httpx.Response(orrespx.MockResponse) for the response and keephttpx2.*for raised exceptions:I can send a PR for the minimal guard change if it would be welcome.