Repository navigation
feat: let HTTPXClient use a caller-owned httpx.AsyncClient - #307
Open
m4xkushnir wants to merge 1 commit into
Open
m4xkushnir wants to merge 1 commit into
m4xkushnir wants to merge 1 commit into
Conversation
Author
|
Hi! This one is opt-in on purpose, so the default client behaves exactly as before and the event-loop and closing questions from #252 stay with the caller. Since #252 was closed in favour of discussing it first, I'm happy to move this to an issue if you'd rather talk the approach through there before reviewing code. The |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HTTPXClientopens a newhttpx.AsyncClienton every request, so async sends never reuse a connection. #252tried to fix that by changing the default and was closed because of the edge cases around event loops and
closing. This takes a narrower route: the default stays exactly as it is, and callers can pass in a client they
own.
client,HTTPXClientstill opens and closes a client per request, so nothing changes for anyonewho doesn't opt in.
client, the SDK never creates, binds or closes it. The caller makes it on their own loop and closes itthemselves, which avoids the two problems fix: reuse HTTP connections in the default clients #252 had to solve. If it's used after being closed, httpx raises a
RuntimeErrorthatAsyncRequestalready turns intoResendError(error_type="HttpClientError").default_async_http_clientis an explicitreplacement.
httpx.MockTransportall come with the caller's client.You can already get this by subclassing
HTTPXClient, but then you're copying the files/json branching and theerror wrapping out of
request(), and that copy drifts when the SDK changes. Here the request body moved into aprivate
_sendhelper that both paths share, and thehttpx.RequestError→RuntimeErrorwrapping coversboth, same as before. I also added a short README note under "Custom async client".
For context, I run Ventute, which sends all its email through Resend. Its weekly digest sends one email per
eligible user, paging users 200 at a time and sending each page with
asyncio.gatherbehind a semaphore of 8.I came across this while looking at moving it from raw
httpxcalls toEmails.send_async.Testing
Against a local HTTP/1.1 server that counts accepted TCP connections, 3 rounds of 8 parallel
Emails.send_asynccalls:HTTPXClient()(default)HTTPXClient(client=client)Against
api.resend.comeach new connection is also a TLS handshake. I only counted connections locally anddidn't measure latency.
Repro (local server, fake key)
tests/http_client_httpx_test.pyuseshttpx.MockTransport:AsyncClientis createdfilesanddatago through a provided clientResendErrorwithHttpClientErrorclient, a newAsyncClientwith the configured timeout is still created per requestThe first three fail on
main, and the last one passes on both. The full suite passes on Python 3.14 and 3.8(855 tests), and flake8 and
tox -e mypyare clean.Two things I left for you to decide. When
clientis passed,timeoutdoesn't apply (the caller's client hasits own), so I documented that rather than raising. And
RequestsClienthas the same per-call behaviour(
requests.request()opens a newSessioneach time), but I kept this PR async-only. Happy to change either.Summary by cubic
Lets
HTTPXClientuse a caller-ownedhttpx.AsyncClientso async sends reuse connections instead of opening a new one per request.Default behavior is unchanged: without
client,HTTPXClientstill opens and closes a client per request. Withclient, the SDK never creates, binds, or closes it, leaving the caller in control of the event loop and lifecycle.timeoutis ignored when a client is provided, and the README documents that.The request body moved into a shared private
_sendhelper so both paths keep the samehttpx.RequestError→RuntimeErrorwrapping.Written for commit 19bd9b5. Summary will update on new commits.