Migrate src/phoenix/ server code from httpx to httpx2. The code footprint is small (6 importing files, ~29 references) but the work is gated on three external dependencies, and it should absorb removal of the dual-stack httpx/httpx2 shim that already exists in the playground.
Current state
The migration is already partially underway:
src/phoenix/server/api/helpers/playground_clients.py:2995-3016 — _HttpxClient(wrapt.ObjectProxy) is an explicit dual-stack shim: the openai SDK is httpx2-based while the other provider SDKs are still httpx-based, so its signatures are unions (wrapped: httpx.AsyncClient | httpx2.AsyncClient, send(request: httpx.Request | httpx2.Request) -> ...). It is injected per-request via client._client = _HttpxClient(client._client, span=span) (:2422-2423), which reaches into SDK internals and is fragile across version bumps of either library. As provider SDKs move to httpx2, this shim should collapse back to a single-stack proxy.
import httpx2 already appears under TYPE_CHECKING at playground_clients.py:105.
httpx2 is not declared in any pyproject — it arrives transitively via the openai SDK chain. Once first-party code imports it at runtime, it must become an explicit dependency in the root pyproject.toml (no pre-release pins, per repo policy).
External blockers (resolve before or alongside)
- FastMCP: capped
<4 at pyproject.toml:62 specifically because FastMCP 4 is an SDK-v2 engine swap that moves to httpx2. src/phoenix/server/mcp_server.py:523-526 builds an httpx.AsyncClient(transport=httpx.ASGITransport(app=_InternalIdentityDispatch(app))) and hands it into FastMCP.from_openapi(...) — the FastMCP 4 uncap and this file's httpx2 port are the same piece of work and must land together.
- authlib:
src/phoenix/server/oauth2.py:16 uses authlib.integrations.httpx_client.AsyncOAuth2Client. Migration of the OAuth path is blocked until authlib ships an httpx2 integration (or we pin the OAuth path to httpx and accept both stacks in the tree — they coexist fine).
- opentelemetry-instrumentation-httpx (
pyproject.toml:128, dev group): needs an httpx2 counterpart for outbound-call instrumentation parity.
Migration sites
src/phoenix/server/mcp_server.py:523-526 — production ASGITransport client (see FastMCP blocker above).
src/phoenix/server/api/helpers/playground_clients.py — retire _HttpxClient union types down to httpx2-only as provider SDKs migrate.
src/phoenix/server/agents/model_factory.py:208, 334 — function-local import httpx; httpx.AsyncClient(headers=...) for the Google GenAI client. Blocked on what client type google-genai accepts.
src/phoenix/server/agents/capabilities/github_mcp.py:120-124 — exception sanitization catching httpx.HTTPStatusError then httpx.HTTPError. Exception-hierarchy-sensitive: the sanitizer exists because raw exception text can leak request headers (:113), so after migration it must catch the httpx2 hierarchy (and possibly both during transition) or sanitization silently stops working.
src/phoenix/trace/fixtures.py:446, 466 — module-level httpx.get(...) / httpx.post(...).
src/phoenix/experimental/datagen/exporter.py:34, 42 — long-lived httpx.Client(headers=..., timeout=...); catches httpx.HTTPError.
Dependency changes
pyproject.toml:44 — replace "httpx[http2]" with the httpx2 equivalent. Confirm httpx2's HTTP/2 support story (built-in vs extra) before dropping the extra.
- Add
httpx2 as an explicit production dependency once runtime imports land.
Behavioral changes to verify
- SSL: httpx2 verifies against the OS trust store (truststore) instead of certifi. Self-hosted deployments with custom CAs may be affected;
SSL_CERT_FILE/SSL_CERT_DIR and verify= still work. Needs a release-notes callout.
- Logging: logger names change to
httpx2 / httpcore2.* — check any logging config that filters by logger name.
- User-Agent becomes
python-httpx2/<version> for outbound calls.
Notes
- No deprecated httpx APIs in use (no
app=, proxies=, allow_redirects=), no httpx.Auth subclasses, no event hooks or mounts in server code — the mechanical port is clean once the blockers clear.
- Unit/integration test suite migration is tracked separately.
Migrate
src/phoenix/server code fromhttpxtohttpx2. The code footprint is small (6 importing files, ~29 references) but the work is gated on three external dependencies, and it should absorb removal of the dual-stack httpx/httpx2 shim that already exists in the playground.Current state
The migration is already partially underway:
src/phoenix/server/api/helpers/playground_clients.py:2995-3016—_HttpxClient(wrapt.ObjectProxy)is an explicit dual-stack shim: the openai SDK is httpx2-based while the other provider SDKs are still httpx-based, so its signatures are unions (wrapped: httpx.AsyncClient | httpx2.AsyncClient,send(request: httpx.Request | httpx2.Request) -> ...). It is injected per-request viaclient._client = _HttpxClient(client._client, span=span)(:2422-2423), which reaches into SDK internals and is fragile across version bumps of either library. As provider SDKs move to httpx2, this shim should collapse back to a single-stack proxy.import httpx2already appears underTYPE_CHECKINGatplayground_clients.py:105.httpx2is not declared in any pyproject — it arrives transitively via the openai SDK chain. Once first-party code imports it at runtime, it must become an explicit dependency in the rootpyproject.toml(no pre-release pins, per repo policy).External blockers (resolve before or alongside)
<4atpyproject.toml:62specifically because FastMCP 4 is an SDK-v2 engine swap that moves to httpx2.src/phoenix/server/mcp_server.py:523-526builds anhttpx.AsyncClient(transport=httpx.ASGITransport(app=_InternalIdentityDispatch(app)))and hands it intoFastMCP.from_openapi(...)— the FastMCP 4 uncap and this file's httpx2 port are the same piece of work and must land together.src/phoenix/server/oauth2.py:16usesauthlib.integrations.httpx_client.AsyncOAuth2Client. Migration of the OAuth path is blocked until authlib ships an httpx2 integration (or we pin the OAuth path to httpx and accept both stacks in the tree — they coexist fine).pyproject.toml:128, dev group): needs an httpx2 counterpart for outbound-call instrumentation parity.Migration sites
src/phoenix/server/mcp_server.py:523-526— productionASGITransportclient (see FastMCP blocker above).src/phoenix/server/api/helpers/playground_clients.py— retire_HttpxClientunion types down to httpx2-only as provider SDKs migrate.src/phoenix/server/agents/model_factory.py:208, 334— function-localimport httpx;httpx.AsyncClient(headers=...)for the Google GenAI client. Blocked on what client typegoogle-genaiaccepts.src/phoenix/server/agents/capabilities/github_mcp.py:120-124— exception sanitization catchinghttpx.HTTPStatusErrorthenhttpx.HTTPError. Exception-hierarchy-sensitive: the sanitizer exists because raw exception text can leak request headers (:113), so after migration it must catch thehttpx2hierarchy (and possibly both during transition) or sanitization silently stops working.src/phoenix/trace/fixtures.py:446, 466— module-levelhttpx.get(...)/httpx.post(...).src/phoenix/experimental/datagen/exporter.py:34, 42— long-livedhttpx.Client(headers=..., timeout=...); catcheshttpx.HTTPError.Dependency changes
pyproject.toml:44— replace"httpx[http2]"with the httpx2 equivalent. Confirm httpx2's HTTP/2 support story (built-in vs extra) before dropping the extra.httpx2as an explicit production dependency once runtime imports land.Behavioral changes to verify
SSL_CERT_FILE/SSL_CERT_DIRandverify=still work. Needs a release-notes callout.httpx2/httpcore2.*— check any logging config that filters by logger name.python-httpx2/<version>for outbound calls.Notes
app=,proxies=,allow_redirects=), nohttpx.Authsubclasses, no event hooks or mounts in server code — the mechanical port is clean once the blockers clear.