# Feature request httpx2

**URL:** <https://discuss.huggingface.co/t/feature-request-httpx2/178664>\
**Category:** 🤗Datasets\
**Created:** [August 14, 2026, 5:58pm UTC](https://discuss.huggingface.co/t/feature-request-httpx2/178664 "2026-08-14T17:58:43Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Chinya](https://avatars.discourse-cdn.com/v4/letter/c/ee7513/32.png) [@Chinya](https://discuss.huggingface.co/u/Chinya)\
**Post date:** [August 14, 2026, 5:58pm UTC](https://discuss.huggingface.co/t/feature-request-httpx2/178664/1 "2026-08-14T17:58:43Z")

</div>

### Feature request

Replace the httpx library with the httpx2 fork, maintained by Pydantic.

### Motivation

Httpx has not been released since December 2024, and has closed all public access to Issues / Discussions / PRs, this raises questions about the actual state of maintenance and could lead to security issues down the line.

[encode/httpx#3784](https://github.com/encode/httpx/discussions/3784)

### 

> <https://github.com/huggingface/datasets/issues/8458>
>
> \### Feature request
> 
> Replace the httpx library with the \[httpx2\](https://github.…com/pydantic/httpx2) fork, maintained by Pydantic.
> 
> \### Motivation
> 
> Httpx has not been released since December 2024, and has closed all public access to Issues / Discussions / PRs, this raises questions about the actual state of maintenance and could lead to security issues down the line.
> 
> https://github.com/encode/httpx/discussions/3784
> 
> 
> \### Your contribution
> 
> /

---

<div class="post-metadata">

**Author:** ![John6666](https://sea2.discourse-cdn.com/hellohellohello/user_avatar/discuss.huggingface.co/john6666/32/27664_2.png) [@John6666](https://discuss.huggingface.co/u/John6666)\
**Post date:** [August 15, 2026, 1:10am UTC](https://discuss.huggingface.co/t/feature-request-httpx2/178664/2 "2026-08-15T01:10:38Z")

</div>

Hmm… after taking a quick look, it seems the Hub internals themselves may be manageable, but there could be some compatibility issues in the surrounding ecosystem…?🤔

* * *

My current impression is that **moving `huggingface_hub` itself from HTTPX to HTTPX2 looks technically quite feasible** , but I would probably treat it as an ecosystem/API migration rather than just a dependency rename.

The important distinction seems to be:

- `httpx` and `httpx2` can coexist in the same environment.
- Their APIs are intentionally very similar.
- But their concrete Python objects are **not interchangeable** : `httpx.Client` is not `httpx2.Client`, `httpx.HTTPError` is not `httpx2.HTTPError`, and the same applies to `Response`, `Request`, transports, timeouts, etc.

That boundary is explicitly documented in the [HTTPX2 migration guide](https://httpx2.pydantic.dev/migration/).

I tried a few focused migration probes against the current Hub code. The encouraging part is that, once the HTTP family was kept coherent all the way down to `httpcore2`, the focused Hub HTTP behavior survived cleanly. The less encouraging part is that some **downstream-visible contracts really do change** , and at least one of those patterns already exists in current Transformers code.

So if this were explored further, my default route would be roughly:

```plaintext
align the Hub HTTP family
        ↓
test in a genuinely HTTPX2-only environment
        ↓
test old-HTTPX + new-HTTPX2 coexistence
        ↓
test the public error/client-factory contracts
        ↓
run a few focused downstream tests

```

The HTTPX2-only environment matters more than it may appear: a mixed environment can be green while old `httpx`/`httpcore` installations quietly mask an incomplete migration.

> **What happened in a small Hub-focused migration probe**
>
> I first tested the obvious partial-migration case: use the new HTTP family in only part of the Hub HTTP path while other pieces still expect the old one.
> 
> A simple `200 OK` path can look fine in that state, which is slightly deceptive. More semantic paths exposed the boundary:
> 
> - Hub status/error normalization
> - the default retry exception tuple
> - custom/mock transports
> 
> Once the same code path was aligned consistently to HTTPX2, those focused contracts recovered.
> 
> There was then another useful clean-install finding.
> 
> The Hub currently has a direct low-level `httpcore` dependency in addition to its higher-level HTTPX usage. HTTPX2’s migration checklist explicitly calls out the corresponding `httpcore -> httpcore2` rename for direct imports: [HTTPX2 migration checklist](https://httpx2.pydantic.dev/migration/#checklist).
> 
> In a true HTTPX2-only environment, leaving that direct old-family dependency in place caused the focused upstream HTTP tests to fail during collection.
> 
> Interestingly, installing old HTTPX alongside HTTPX2 made the same tests pass, because old HTTPX brought old `httpcore` back into the environment. In other words:
> 
> ```plaintext
> HTTPX2-only
> → stale direct httpcore dependency is visible
> 
> HTTPX2 + old HTTPX
> → old HTTPX supplies httpcore
> → stale dependency is masked
> 
> ```
> 
> After aligning both:
> 
> ```plaintext
> httpx -> httpx2
> httpcore -> httpcore2
> 
> ```
> 
> the clean environment really contained neither old `httpx` nor old `httpcore`, the focused runtime contract passed, and the HTTP-focused upstream test logic passed **64/64** after the corresponding family-aligned imports.
> 
> I would not interpret that as “the whole Hub is already proven HTTPX2-compatible”; it is narrower than that. But it does suggest that there is no obvious fundamental incompatibility in the core Hub HTTP layer itself.
> 
> It also suggests that a migration CI matrix should include a **true new-family-only job** , rather than relying only on an environment where both packages happen to be installed.

The part I would look at most carefully is actually the public/customization surface.

The current [`huggingface_hub` HTTP utilities](https://huggingface.co/docs/huggingface_hub/en/package_reference/utilities) explicitly expose HTTPX types:

- `get_session()` returns a configured `httpx.Client`.
- `set_client_factory()` accepts a factory returning an `httpx.Client`.
- equivalent async APIs expose `httpx.AsyncClient`.
- third-party libraries are specifically encouraged to use `get_session()` for direct Hub requests.

So those HTTPX types are not completely hidden implementation details.

> **Three downstream-visible compatibility breaks I could reproduce**
>
> ### 1. `HfHubHTTPError` and `except httpx.HTTPError`
> 
> The current Hub error hierarchy refines the HTTP backend error. See [`huggingface_hub/errors.py`](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/errors.py).
> 
> Today, code can effectively rely on:
> 
> ```python
> try:
> ...
> except httpx.HTTPError:
> ...
> 
> ```
> 
> also catching HTTP-derived Hub errors.
> 
> If the Hub implementation moves to HTTPX2, a naturally migrated hierarchy becomes:
> 
> ```plaintext
> HfHubHTTPError
> └── httpx2.HTTPError
> 
> ```
> 
> and that is a different Python class family from:
> 
> ```plaintext
> httpx.HTTPError
> 
> ```
> 
> In the coexistence probe, this was not just theoretical:
> 
> ```python
> issubclass(HfHubHTTPError, httpx.HTTPError)
> 
> ```
> 
> became false, while the corresponding `httpx2.HTTPError` relationship was true.
> 
> So code that catches the Hub-specific type is relatively easy to preserve:
> 
> ```python
> except HfHubHTTPError:
> ...
> 
> ```
> 
> as are more specific Hub errors such as:
> 
> ```python
> except EntryNotFoundError:
> ...
> 
> except RepositoryNotFoundError:
> ...
> 
> ```
> 
> But generic catches such as:
> 
> ```python
> except httpx.HTTPError:
> ...
> 
> ```
> 
> are a genuine migration boundary.
> 
> This is quite similar to something HF already had to document during the previous `requests -> httpx` migration. The [Hub v1.0 migration guide](https://huggingface.co/docs/huggingface_hub/concepts/migration) explicitly notes that HTTP errors stopped inheriting from `requests.HTTPError` and started inheriting from `httpx.HTTPError`, and recommends catching `HfHubHTTPError` when possible.
> 
> Conceptually, HTTPX2 can create the next version of the same boundary:
> 
> ```plaintext
> requests.HTTPError
> ↓
> httpx.HTTPError
> ↓
> httpx2.HTTPError
> 
> ```
> 
> The first migration was much larger internally; this one appears much smaller internally, but the Python class-identity issue is still real.
> 
> ### 2. `get_session()` changes concrete client type
> 
> Current Hub code and documentation expose:
> 
> ```python
> get_session() -> httpx.Client
> 
> ```
> 
> See [`_http.py`](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/utils/_http.py).
> 
> With an HTTPX2-backed Hub, the natural result is:
> 
> ```python
> get_session() -> httpx2.Client
> 
> ```
> 
> The probe confirmed the corresponding type break:
> 
> ```python
> isinstance(get_session(), httpx.Client)
> 
> ```
> 
> becomes false.
> 
> For most application code that just calls:
> 
> ```python
> get_session().get(...)
> 
> ```
> 
> that may make no practical difference.
> 
> But it matters to code that:
> 
> - performs `isinstance` checks,
> - annotates/constrains HTTP client types,
> - passes the client into another library that expects old `httpx.Client`,
> - or catches old-family exceptions coming from that client.
> 
> ### 3. Existing custom client factories
> 
> This was the clearest runtime compatibility example.
> 
> The Hub currently documents patterns conceptually like:
> 
> ```python
> from huggingface_hub import set_client_factory
> import httpx
> 
> set_client_factory(lambda: httpx.Client(...))
> 
> ```
> 
> I tried leaving such an existing old-HTTPX factory unchanged while migrating the Hub’s error handling to HTTPX2.
> 
> With an old `httpx.Client`, a synthetic 404 produced an old:
> 
> ```plaintext
> httpx.HTTPStatusError
> 
> ```
> 
> and the HTTPX2-aligned Hub error-normalization path did **not** convert it into `HfHubHTTPError`.
> 
> Changing only the custom factory to:
> 
> ```python
> httpx2.Client(...)
> 
> ```
> 
> restored the expected Hub normalization.
> 
> So this one seems worth considering a concrete public migration requirement rather than only an internal implementation concern.

There is also at least one particularly direct downstream example in current Transformers.

> **A concrete downstream pattern already exists in Transformers**
>
> Current [`transformers/utils/hub.py`](https://github.com/huggingface/transformers/blob/main/src/transformers/utils/hub.py) contains a path equivalent to:
> 
> ```python
> try:
> response = get_session().head(...)
> except httpx.ProxyError:
> raise
> except (httpx.ConnectError, httpx.TimeoutException, OfflineModeIsEnabled):
> ...
> 
> ```
> 
> This is an interesting case because the two sides of the contract come from different packages:
> 
> ```plaintext
> huggingface_hub.get_session()
> ↓
> HTTP client
> ↓
> Transformers catches httpx.* exceptions
> 
> ```
> 
> With the current Hub implementation those belong to the same HTTPX family.
> 
> If `get_session()` starts returning an `httpx2.Client` while Transformers remains on old HTTPX, connection failures from that client naturally belong to the `httpx2.*` exception hierarchy, not `httpx.*`.
> 
> So this looks like a **real downstream migration site** , not merely a hypothetical user pattern.
> 
> I have not claimed a full Transformers runtime regression here — the focused probe tested the underlying class/exception contract rather than forcing this exact Transformers function through every network failure mode — but the source-level dependency is unusually direct.
> 
> That is probably the kind of downstream contract I would include in an acceptance test before switching the Hub default.

Datasets also looks worth including in such a matrix, although I would be more careful about what is claimed there.

Current [`datasets/load.py`](https://github.com/huggingface/datasets/blob/main/src/datasets/load.py) imports both raw HTTPX and Hub HTTP utilities / Hub-specific exceptions, including `get_session()`.

Current [`datasets/utils/file_utils.py`](https://github.com/huggingface/datasets/blob/main/src/datasets/utils/file_utils.py) also includes `httpx.RequestError` in its retryable connection-error tuple.

That makes Datasets another concrete migration surface, but I would distinguish this from proving an actual Datasets streaming regression. I have **not** demonstrated a real streaming failure caused by HTTPX2, so I would keep that as something to test rather than a conclusion.

> **safehttpx / Transport boundary**
>
> I also checked `safehttpx`, because custom transports are another place where the two package families meet.
> 
> A cross-family setup failed as expected: an old-HTTPX transport/object cannot simply be handed across an HTTPX2 boundary (and vice versa).
> 
> That behavior is consistent with the HTTPX2 documentation:
> 
> > the packages can coexist, but their objects cannot stand in for one another.
> 
> After aligning `safehttpx` to the same family, the focused behavior recovered.
> 
> I then reran its upstream async tests with the async pytest plugin explicitly installed; this was important because an earlier run without the proper async test plugin produced failures that were not actually diagnostic of HTTPX2 compatibility.
> 
> With the test environment corrected:
> 
> ```plaintext
> current HTTPX 19/19
> HTTPX2-only 19/19
> HTTPX2 + old HTTPX 19/19
> 
> ```
> 
> So I do **not** currently see evidence that `safehttpx` itself fundamentally blocks HTTPX2.
> 
> The narrower conclusion is just that a Transport, Response, Client, or exception should remain in one HTTP family for a given code path.

### This looks easier than `requests -> httpx`, but not completely trivial

I would separate two kinds of difficulty.

For the **Hub internals** , this looks substantially easier than the previous `requests -> httpx` migration. HTTPX2 is a fork of HTTPX 0.28.1 and deliberately keeps essentially the same public API; its migration guide even recommends `import httpx2 as httpx` as a low-churn first pass for larger codebases.

The previous Hub migration involved genuine semantic/API changes around proxies, SSL errors, sync/async clients, custom backend configuration, and error inheritance. HF documents those differences in the [v1.0 migration guide](https://huggingface.co/docs/huggingface_hub/concepts/migration).

HTTPX2 does not appear to require another rewrite of that scale.

For the **ecosystem boundary** , though, it is less trivial than the almost-identical APIs might suggest, because Python class identity changes while code still looks nearly identical.

That can produce an awkward transitional state such as:

```plaintext
Transformers ──> httpx
Datasets ──> httpx
Gradio ──> httpx
Hub ──> httpx2

```

This environment installs perfectly well.

But if an `httpx2.Client`, `httpx2.Response`, or `httpx2.HTTPError` crosses into code written against `httpx.*`, the mismatch only appears at runtime.

That is arguably easier to overlook than the old requests/HTTPX transition, where the two APIs were visibly more different.

> **Possible migration shapes**
>
> HTTPX2’s own package-maintainer guidance gives two useful broad strategies.
> 
> ### A. A breaking/major transition is acceptable
> 
> Then the simple route is probably:
> 
> ```plaintext
> replace httpx with httpx2
> ↓
> replace direct httpcore with httpcore2
> ↓
> update public client/factory types
> ↓
> document the exception-type transition
> ↓
> update downstream users that catch raw httpx exceptions
> 
> ```
> 
> For the Hub, I would add clean-only and downstream contract tests before considering that complete.
> 
> ### B. A breaking transition is not desirable yet
> 
> HTTPX2 documents a dual-support pattern where a module prefers `httpx2` and falls back to `httpx`, while ensuring a given code path uses only one family.
> 
> [Starlette has already added HTTPX2 support](https://github.com/Kludex/starlette/blob/main/docs/release-notes.md), so there is a real project precedent for a staged transition.
> 
> Something conceptually like:
> 
> ```python
> try:
> import httpx2 as httpx
> except ModuleNotFoundError:
> import httpx
> 
> ```
> 
> can make a module family-consistent whichever backend is present.
> 
> Whether that pattern makes sense for the Hub’s central HTTP layer is a separate design decision, because the Hub deliberately exposes the client factory and client type to downstream users. But it is at least an available migration shape.
> 
> HTTPX2 also provides `alias_httpx()`, but its documentation explicitly says that this is an **application-only** escape hatch, not something a library should impose process-wide. So I would not see that as an appropriate compatibility shortcut inside `huggingface_hub` itself.

### A small test matrix may be enough to make the scope concrete

If this were prototyped as a Hub migration, I think four environments would catch most of the important classes of problem:

| Environment | What it catches |
| --- | --- |
| current HTTPX baseline | ordinary regression baseline |
| true HTTPX2-only | stale direct `httpx` / `httpcore` dependencies |
| HTTPX2 Hub + old HTTPX installed | staggered ecosystem / coexistence behavior |
| HTTPX2 Hub + current major downstream libs | public-contract breakage |

For each one, a relatively small set of focused checks may be more informative than a huge generic smoke test:

```plaintext
200 response
404 -> HfHubHTTPError
network retry
get_session()
get_async_session()
set_client_factory()
set_async_client_factory()
custom Transport
Hub-specific exception catches
raw httpx exception catches

```

Then a few targeted downstream paths:

```plaintext
Transformers
Datasets
Diffusers
Gradio / safehttpx

```

The clean-only job is especially useful because of the stale-`httpcore` masking case above.

> **A few other HTTPX2-specific details**
>
> The [HTTPX2 migration checklist](https://httpx2.pydantic.dev/migration/#checklist) also calls out some smaller visible changes worth including in a real migration review:
> 
> - `httpcore` becomes `httpcore2` for direct imports.
> - logger names become `httpx2` and `httpcore2.*`.
> - the default User-Agent becomes `python-httpx2/...`.
> - HTTPX2 uses the operating-system trust store by default.
> - custom CA / proxy setups should therefore get a focused check.
> - Python 3.10+ is required.
> 
> Some surrounding tooling is already moving: for example, recent Starlette releases include HTTPX2 support, so I would not assume the broader Python HTTP ecosystem is generally incompatible.
> 
> But instrumentation, mocking, client injection, and custom transport integrations are exactly the places where I would check concrete types rather than assuming API similarity is sufficient.

So overall, I think the feature request is technically quite plausible.

What I would _not_ assume is that the migration unit is just:

```diff
- httpx
+ httpx2

```

The experiments so far point more toward:

```plaintext
Hub internal HTTP family
    → fairly straightforward to migrate coherently

public HTTPX types / exceptions / client factories
    → actual compatibility boundary

downstream libraries
    → a small number of focused changes/tests may be needed

```

That still looks much less invasive internally than the original `requests -> httpx` work. The main work seems to be deciding how to handle the existing public HTTPX-facing contracts and the staggered period where the Hub and its downstream libraries may be on different package families.

A true HTTPX2-only CI job plus a small downstream compatibility matrix would probably make the remaining scope much easier to judge.
