Skip to content

Add support for httpx as backend #1085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 11, 2025
Merged

Add support for httpx as backend #1085

merged 4 commits into from
Jun 11, 2025

Conversation

jakkdl
Copy link
Contributor

@jakkdl jakkdl commented Feb 2, 2024

First step of #749 as described in #749 (comment)

I was tasked with implementing this, but it's been a bit of a struggle not being very familiar with aiohttp, httpx or aiobotocore - and there being ~zero in-line types. But I think I've fixed enough of the major problems that it's probably useful to share my progress.

There's a bunch of random types added. I can split those off into a separate PR or remove if requested. Likewise for from __future__ import annotations.

TODO:

  • exceptions
    • retryable_exceptions: mostly just need to go through all httpx exceptions and decide which ones are fine
    • The mapping between httpx exceptions and aiobotocore exceptions can likely be improved.
      # **previous exception mapping**
      # aiohttp.ClientSSLError -> SSLError
      # aiohttp.ClientProxyConnectiorError
      # aiohttp.ClientHttpProxyError -> ProxyConnectionError
      # aiohttp.ServerDisconnectedError
      # aiohttp.ClientPayloadError
      # aiohttp.http_exceptions.BadStatusLine -> ConnectionClosedError
      # aiohttp.ServerTimeoutError -> ConnectTimeoutError|ReadTimeoutError
      # aiohttp.ClientConnectorError
      # aiohttp.ClientConnectionError
      # socket.gaierror -> EndpointConnectionError
      # asyncio.TimeoutError -> ReadTimeoutError
      # **possible httpx exception mapping**
      # httpx.CookieConflict
      # httpx.HTTPError
      # * httpx.HTTPStatusError
      # * httpx.RequestError
      # * httpx.DecodingError
      # * httpx.TooManyRedirects
      # * httpx.TransportError
      # * httpx.NetworkError
      # * httpx.CloseError -> ConnectionClosedError
      # * httpx.ConnectError -> EndpointConnectionError
      # * httpx.ReadError
      # * httpx.WriteError
      # * httpx.ProtocolError
      # * httpx.LocalProtocolError -> SSLError??
      # * httpx.RemoteProtocolError
      # * httpx.ProxyError -> ProxyConnectionError
      # * httpx.TimeoutException
      # * httpx.ConnectTimeout -> ConnectTimeoutError
      # * httpx.PoolTimeout
      # * httpx.ReadTimeout -> ReadTimeoutError
      # * httpx.WriteTimeout
      # * httpx.UnsupportedProtocol
      # * httpx.InvalidURL
      except httpx.ConnectError as e:
      raise EndpointConnectionError(endpoint_url=request.url, error=e)
      except (socket.gaierror,) as e:
      raise EndpointConnectionError(endpoint_url=request.url, error=e)
      except asyncio.TimeoutError as e:
      raise ReadTimeoutError(endpoint_url=request.url, error=e)
      except httpx.ReadTimeout as e:
      raise ReadTimeoutError(endpoint_url=request.url, error=e)
      except NotImplementedError:
      raise
      except Exception as e:
      message = 'Exception received when sending urllib3 HTTP request'
      logger.debug(message, exc_info=True)
      raise HTTPClientError(error=e)
  • proxy support
    • postponed to later PR
    • this was previously handled per-request, but AFAICT you can only configure proxies per-client in httpx. So need to move the logic for it, and cannot use botocore.httpsession.ProxyConfiguration.proxy_[url,headers]_for(request.url)
    • raising of ProxyConnectionError is very ugly atm, and probably not "correct"?
    • BOTO_EXPERIMENTAL__ADD_PROXY_HOST_HEADER
      • seems not possible to do when configuring proxies per-client?
  • wrap io.IOBase data in a non-sync-iterable async iterable
    • converted to bytes for now.
  • I have added change info to CHANGES.rst

No longer TODOs after changing the scope to implement httpx alongside aiohttp:

  • test_patches previously cared about aiohttp. That can probably be retired?
  • replace aiohttp with httpx in tests.mock_server.AIOServer?
  • The following connector_args now raise NotImplementedError:
    • use_dns_cache: did not find any mentions of dns caches on a quick skim of httpx docs
    • force_close: same. Can maybe find out more by digging into docs on what this option does in aiohttp.
    • resolver: this is an aiohttp.abc.AbstractResolver which is obviously a no-go.
      • raise error for code passing this
      • figure out equivalent functionality for httpx
  • url's were previously wrapped with yarl.URL(url, encoding=True). httpx does not support yarl. I don't know what this achieved (maybe the non-normalization??), so skipping it for now.

Some extra tests would probably also be good, but not super critical when we're just implementing httpx alongside aiohttp.

@jakkdl
Copy link
Contributor Author

jakkdl commented Feb 6, 2024

I started wondering whether response.StreamingBody should wrap httpx.Response or one of its iterators (aiter_bytes, aiter_text, aiter_lines or aiter_raw), but am now starting to think that maybe it doesn't make sense to have at all and we should just surface the httpx.Response object to the user and let them handle it as they want.

The way that aiohttp.StreamReader behaves is just different enough that providing a translation layer that handles httpx.Response streams the same way becomes quite clunky/inefficient/tricky/very different. StreamingBody.iter_chunks should be done by specifying chunk size when calling httpx.Response.aiter_bytes, and StreamingBody.iter_lines should use httpx.Response.aiter_lines, but the current API does nothing to stop you from reading one chunk, then one byte, but httpx.Response (very reasonably) only lets you initialize the iterators once.
Implementing iter_chunks/iter_lines/etc as reading one byte at a time with await anext() on an aiter_raw sounds awful, since there's no read() method that can return a set number of bytes. That in general makes StreamingBody.read() quite clunky to implement.

Copy link

codecov bot commented Feb 19, 2024

Codecov Report

Attention: Patch coverage is 77.90368% with 78 lines in your changes missing coverage. Please review.

Project coverage is 90.59%. Comparing base (e3c37ee) to head (25351b7).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
aiobotocore/httpxsession.py 73.72% 36 Missing ⚠️
tests/test_basic_s3.py 64.15% 19 Missing ⚠️
aiobotocore/httpchecksum.py 65.71% 12 Missing ⚠️
tests/conftest.py 82.45% 10 Missing ⚠️
aiobotocore/response.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1085      +/-   ##
==========================================
- Coverage   91.12%   90.59%   -0.53%     
==========================================
  Files          66       67       +1     
  Lines        6994     7303     +309     
==========================================
+ Hits         6373     6616     +243     
- Misses        621      687      +66     
Flag Coverage Δ
no-httpx 89.01% <45.89%> (?)
os-ubuntu-24.04 90.59% <77.90%> (-0.53%) ⬇️
os-ubuntu-24.04-arm 90.31% <72.23%> (-0.81%) ⬇️
python-3.10 90.23% <70.82%> (-0.88%) ⬇️
python-3.11 90.30% <72.23%> (-0.81%) ⬇️
python-3.12 90.30% <72.23%> (-0.81%) ⬇️
python-3.13 90.57% <77.90%> (-0.53%) ⬇️
python-3.9 90.29% <72.23%> (-0.81%) ⬇️
unittests 90.59% <77.90%> (-0.53%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jakkdl jakkdl changed the title Replace aiohttp with httpx Add support for httpx as alternate backend Feb 19, 2024
@jakkdl jakkdl changed the title Add support for httpx as alternate backend Add support for httpx as backend Feb 19, 2024
@jakkdl
Copy link
Contributor Author

jakkdl commented Feb 19, 2024

Whooooo, all tests are passing!!!!
though I did an ugly with test_non_normalized_key_paths - I understand nothing about that test so I currently made the test pass if httpx returns a normalized path.

current TODOs:

  • I should add a command-line parameter that sets the http backend to be tested, so I can set up a CI environment without httpx installed to make sure that works.
  • Retryable exceptions.
    • Maybe try to write a test for it
  • figure out the branches in convert_to_response_dict.
    • I think they're fine?
  • ~~figure out proxies, or ~~raise NotImplementedError.
    • There is at least one test that sorta checks it so if raising I need to work around it.
  • Maybe add test for http_session_cls
  • Add documentation - RTD is broken?

codecov is very sad, but most of that is due to me duplicating code that wasn't covered to start with, or extending tests that aren't run in CI. I'll try to make it very slightly less sad, but making it completely unsad is very much out of scope for this PR.

Likewise RTD is failing ... and I think that's unrelated to the PR?

@jakkdl jakkdl marked this pull request as ready for review February 20, 2024 12:28
@jakkdl
Copy link
Contributor Author

jakkdl commented Feb 21, 2024

@thejcannon @aneeshusa if you wanna do a review pass

@jakkdl jakkdl requested a review from thehesiod March 1, 2024 10:26
@jakkdl
Copy link
Contributor Author

jakkdl commented Mar 20, 2024

Hey @thehesiod what's the feeling on this? It is turning out to be a messier and more disruptive change than initially thought in #749. I can pull out some of the changes to a separate PR to make this a bit smaller at least

@thehesiod
Copy link
Collaborator

hey sorry been down with a cold, will look asap. I don't mind big PRs

@thehesiod
Copy link
Collaborator

btw check out: https://awslabs.github.io/aws-crt-python/api/http.html#awscrt.http.HttpClientConnection perhaps we should go in that direction so it will be complete AWS impl

@thehesiod
Copy link
Collaborator

discussion here: #1106

@thehesiod
Copy link
Collaborator

back from my trip, will look asap, also need to add Jacob as a helper to review these, so much to do, so little time ;)

@jakob-keller jakob-keller added the enhancement New feature or request label Aug 20, 2024
@jakkdl jakkdl requested a review from thehesiod August 31, 2024 12:52
@jakkdl
Copy link
Contributor Author

jakkdl commented Oct 19, 2024

CI failure is/was pre-commit/pre-commit-hooks#1101

@jakkdl
Copy link
Contributor Author

jakkdl commented Oct 19, 2024

I could make some small improvements to the codecov (but a lot of it is because I've duplicated previously uncovered code), but otherwise I'm still mostly just waiting for review :)

@thehesiod
Copy link
Collaborator

sorry for taking so long, mind updating this pr? looks pretty good, just needs the in-depth review I promised (and have failed to provide yet, sorry!)

@jakkdl
Copy link
Contributor Author

jakkdl commented Feb 27, 2025

sorry for taking so long, mind updating this pr? looks pretty good, just needs the in-depth review I promised (and have failed to provide yet, sorry!)

since switching over to uv I'm not quite sure how to properly do a run in CI where httpx isn't installed, but otherwise I think this should be good. I've definitely forgotten .. a lot of details though 😅

Comment on lines 57 to 63
if: ${{ ! inputs.no-httpx }}
env:
COLOR: 'yes'
run: |
uv run -v --all-extras make mototest
- name: Run unittests without httpx installed
if: ${{ inputs.no-httpx }}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the normal tests run with boto3 and awscli? I don't think they did previously but uv is still confusing me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, they are not installed as part of CI.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we run three sets of tests to cover everything?

  1. without httpx installed - corresponds to current test suite
  2. with httpx installed, but not configured to be used by aiobotocore - to protect against regressions from changes introduced as part of this PR
  3. with httpx installed and configured to be used by aiobotocore - test experimental functionality introduced by this PR

httpx is specified as an optional dependency and is easily installed by using uv run --extra httpx ..., see uv docs.

I'm not very familiar with pytest configuration, but it looks like you have figured that part out already.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the second part is effectively tested through the way pytest is currently parametrizing tests, --http-backend=all runs each test twice

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool @jakob-keller you satisfied with where we're at with this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've stated how I would set up the test suite:

Why don't we run three sets of tests to cover everything?

  1. without httpx installed - corresponds to current test suite
  2. with httpx installed, but not configured to be used by aiobotocore - to protect against regressions from changes introduced as part of this PR
  3. with httpx installed and configured to be used by aiobotocore - test experimental functionality introduced by this PR

As I understand the proposed test suite, two jobs are run which cover parts of that proposed strategy:

  • The job Run unittests mixes 2. and 3. I don't feel qualified to judge if that is properly implemented.
  • The job Run unittests without httpx installed covers 1 - at least partially.

It seems to work and CI passes. I'm really not in a position to go beyond that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we have one CI job on each python version with --http-backend=all that tests both the aiohttp and httpx backend. This is not perfect currently, as I noticed some tests that directly create internal classes to test them (instead of using e.g. the s3_client fixture). I suspect some minor bugs in the httpx backend are sneaking through because of this, but the tests will need bigger overhauls with #749 so I don't see much reason to delay this PR for now. There should be no risk that issues with the aiohttp backend are sneaking through.

We only run a single CI job for checking without httpx installed, which is mostly just to catch ImportError. I don't see any reason to extend this to testing on all python versions, and this is what I've done in e.g. pytest. (I'm assuming this is what you mean by "partially").

tl;dr no this isn't perfectly testing httpx, but we're explicitly calling it an experimental release and I plan on continuing work once this PR gets merged.

@jakkdl jakkdl requested a review from jakob-keller June 3, 2025 13:58
Comment on lines 57 to 63
if: ${{ ! inputs.no-httpx }}
env:
COLOR: 'yes'
run: |
uv run -v --all-extras make mototest
- name: Run unittests without httpx installed
if: ${{ inputs.no-httpx }}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we run three sets of tests to cover everything?

  1. without httpx installed - corresponds to current test suite
  2. with httpx installed, but not configured to be used by aiobotocore - to protect against regressions from changes introduced as part of this PR
  3. with httpx installed and configured to be used by aiobotocore - test experimental functionality introduced by this PR

httpx is specified as an optional dependency and is easily installed by using uv run --extra httpx ..., see uv docs.

I'm not very familiar with pytest configuration, but it looks like you have figured that part out already.

@jakkdl jakkdl requested a review from jakob-keller June 4, 2025 10:44
Copy link
Collaborator

@jakob-keller jakob-keller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for pushing this forward!

All my comments are in and many have been addressed. I do not feel familiar enough with the test suite to give qualified feedback there beyond what I already provided. From my perspective, there are no blocking issues remaining.

From a technical perspective, the PR needs to be rebased unto main and all commit need verified signatures.

As soon as that is completed, I would like to ask @thehesiod and/or @webknjaz to perform a final review and their approval.

@jakkdl
Copy link
Contributor Author

jakkdl commented Jun 5, 2025

squashed to get it all into a verified commit, and merged main. @jakob-keller you'll need to approve to unblock the CI as well since you requested changes in a previous review.

@jakkdl jakkdl requested a review from thehesiod June 5, 2025 09:36
@jakob-keller jakob-keller self-requested a review June 5, 2025 09:47
@jakob-keller
Copy link
Collaborator

squashed to get it all into a verified commit, and merged main. @jakob-keller you'll need to approve to unblock the CI as well since you requested changes in a previous review.

Can you dismiss the review? I don't know, how to do that.

@jakob-keller jakob-keller removed their request for review June 5, 2025 09:52
@jakkdl jakkdl requested a review from jakob-keller June 5, 2025 11:16
@jakkdl
Copy link
Contributor Author

jakkdl commented Jun 5, 2025

squashed to get it all into a verified commit, and merged main. @jakob-keller you'll need to approve to unblock the CI as well since you requested changes in a previous review.

Can you dismiss the review?

I don't think I can.

I don't know, how to do that.

Files changed -> Review changes [green button top right] -> tick the approve box -> submit review

@jakob-keller
Copy link
Collaborator

Files changed -> Review changes [green button top right] -> tick the approve box -> submit review

Sorry, but I don't feel qualified to approve this PR in its entirety.

@jakkdl
Copy link
Contributor Author

jakkdl commented Jun 5, 2025

Files changed -> Review changes [green button top right] -> tick the approve box -> submit review

Sorry, but I don't feel qualified to approve this PR in its entirety.

no yeah I get that, but I think you need to do that at some point to unblock CI even if it gets approved by thehesiod/webknjaz

@jakob-keller
Copy link
Collaborator

Files changed -> Review changes [green button top right] -> tick the approve box -> submit review

Sorry, but I don't feel qualified to approve this PR in its entirety.

no yeah I get that, but I think you need to do that at some point to unblock CI even if it gets approved by thehesiod/webknjaz

I think it's only the "conversations" that need to be marked as resolved. This is independent of any approvals.

@thehesiod
Copy link
Collaborator

cool where do we stand, I can re-review. I was unsure as to the build changes as that was recently revamped. if those are good I can look at the rest

@thehesiod
Copy link
Collaborator

ah just caught up. will re-review later today

@thehesiod
Copy link
Collaborator

@jakkdl we have some build failures, 3.14

@jakob-keller
Copy link
Collaborator

@jakkdl we have some build failures, 3.14

These are experimental tests and are currently expected to fail. Don't worry about it.

@thehesiod thehesiod merged commit 25e3292 into aio-libs:master Jun 11, 2025
18 of 21 checks passed
@thehesiod
Copy link
Collaborator

awesome work guys, sorry for delay, crazy as usual here :)

@thehesiod
Copy link
Collaborator

aaand published ;). @jakob-keller I had to manually approve the pypi deploy on master, is that normal process now? seems odd

@thehesiod
Copy link
Collaborator

another weird thing is it then had a 15m wait timer for "protection rules"

@jakob-keller
Copy link
Collaborator

Yes, we've had this for a while now. I believe @webknjaz put that in last year when we redid CI/CD.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants