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.
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:
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.
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:
HTTPX2-only
→ stale direct httpcore dependency is visible
HTTPX2 + old HTTPX
→ old HTTPX supplies httpcore
→ stale dependency is masked
After aligning both:
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 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.
Today, code can effectively rely on:
try:
...
except httpx.HTTPError:
...
also catching HTTP-derived Hub errors.
If the Hub implementation moves to HTTPX2, a naturally migrated hierarchy becomes:
HfHubHTTPError
└── httpx2.HTTPError
and that is a different Python class family from:
httpx.HTTPError
In the coexistence probe, this was not just theoretical:
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:
except HfHubHTTPError:
...
as are more specific Hub errors such as:
except EntryNotFoundError:
...
except RepositoryNotFoundError:
...
But generic catches such as:
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 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:
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:
get_session() -> httpx.Client
See _http.py.
With an HTTPX2-backed Hub, the natural result is:
get_session() -> httpx2.Client
The probe confirmed the corresponding type break:
isinstance(get_session(), httpx.Client)
becomes false.
For most application code that just calls:
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:
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:
httpx.HTTPStatusError
and the HTTPX2-aligned Hub error-normalization path did not convert it into HfHubHTTPError.
Changing only the custom factory to:
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 contains a path equivalent to:
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:
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 imports both raw HTTPX and Hub HTTP utilities / Hub-specific exceptions, including get_session().
Current 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:
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.
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:
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:
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, so there is a real project precedent for a staged transition.
Something conceptually like:
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:
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:
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 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:
- httpx
+ httpx2
The experiments so far point more toward:
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.