Skip to content

fix: map InvalidURL in build_request; clear TypeError for sync-stream middleware responses - #189

Merged
lesnik512 merged 1 commit into
mainfrom
fix/client-entry-errors
Oct 17, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/client-entry-errors

Conversation

@lesnik512

Copy link
Copy Markdown
Member

What changed

#175. Client.build_request and AsyncClient.build_request now raise TransportError for an invalid URL, with the httpx2.InvalidURL as __cause__. The verb methods, request() and the *_with_response methods already did this. The try/except InvalidURL that lived in both _prepare_request methods has moved into the shared _build_request helper, so all four call sites go through one mapping via map_httpx2_exception. stream() also calls _build_request and raised TransportError before this change too, because its outer except MAPPED_HTTPX2_ERRORS caught the raw error. The build_request docstrings now say what an invalid URL raises. docs/errors.md already says an invalid URL raises TransportError from every method, and that is now true.

#176. When AsyncClient middleware returns an unread response whose stream is not an httpx2.AsyncByteStream, send() and send_with_response() (and so every verb with response_model) raise:

TypeError: AsyncClient middleware returned a response with a sync stream; AsyncClient middleware must return async-streamed responses

The check runs before await response.aread(). Before this change the caller got RuntimeError: Attempted to call an async iterator on a sync stream. from inside httpx2.

httpx2.AsyncByteStream and httpx2.SyncByteStream are both public exports. ByteStream (what content=b"..." produces) subclasses both, so in-memory bodies pass either check.

Sync mirror. Client has the same problem in a different form. A sync middleware returning a response built from an async iterator reached _BoundDecoder.decode, whose response.read() raised RuntimeError: Attempted to call a sync iterator on an async stream., which got wrapped as DecodeError. That pointed at the decoder, though the decoder had nothing to do with it. Client._send and _send_with_response now raise the mirrored TypeError ("Client middleware returned a response with an async stream; Client middleware must return sync-streamed responses"). A test also pins the existing behaviour that an unread sync-stream response from sync middleware still decodes.

docs/middleware.md gets one paragraph about this next to the existing sync/async middleware rule.

Performance

Both checks sit behind if not response.is_closed. A response from httpx2's own send() (not streamed) and one built with content=bytes are already closed, so the isinstance only runs for unread middleware-built responses. On the async side the guard was already there. On the sync side it adds one attribute read per decoded request (about 7 ns on its own in timeit).

Mocked get("/", response_model=dict) over httpx2.MockTransport, 20k calls x 7 rounds, min per call:

before after
AsyncClient 39,621 / 40,049 ns 39,770 / 39,699 ns
Client 38,114 / 37,954 ns 37,246 / 37,214 ns

(two runs each.) The differences are within noise.

Closes #175
Closes #176

… middleware responses

build_request on both clients now raises TransportError for an invalid URL,
with the httpx2.InvalidURL as __cause__, like every other entry point.

When AsyncClient middleware returns an unread response with a sync stream,
send() and send_with_response() raise a TypeError that names the problem
instead of the RuntimeError from inside httpx2. Client gets the mirror check
for an async-stream response, which used to surface as a DecodeError.

Closes #175
Closes #176
@lesnik512
lesnik512 merged commit 660146e into main Oct 17, 2026
13 checks passed
@lesnik512
lesnik512 deleted the fix/client-entry-errors branch October 17, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant