Skip to content

[server] Migrate the Phoenix server from httpx to httpx2 and retire the dual-stack shim #15783

Description

@mikeldking

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)

  1. 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.
  2. 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).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    c/serverServer implementationenhancementNew feature or request

    Type

    No type

    Projects

    • Status
      📘 Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions