Repository navigation
Description
httpx sees little maintenance, and the ecosystem is moving to httpx2, Pydantic's maintained fork of httpx 0.28.1. openai 3.x already requires httpx2, starlette's TestClient prefers it, and opentelemetry-instrumentation-httpx supports it. dstack already uses httpx2 for gateway auth, for its pool fixes (#4338), while the shared proxy code still uses httpx, so the gateway runs both libraries and their exception classes don't mix.
| import httpx2 as httpx |
We currently use three HTTP clients:
requests(+requests-unixsocket):APIClient(CLI and Python API), backend API clients, runner/shim client, docker registry, REST plugin.httpx: server and gateway proxy, gateway client, probes, router worker sync, job replica client, fluentbit, vastai.httpx2: gateway auth and gateway server client.
Proposal
- Replace
httpxwithhttpx2everywhere. This is mostly mechanical, since httpx2 keeps the httpx API. It includes switching the OTel instrumentor toHTTPX2ClientInstrumentor, whichHTTPXClientInstrumentordoesn't cover:
dstack/src/dstack/_internal/server/utils/otel/utils.py
Lines 106 to 112 in d736e86
| SQLAlchemyInstrumentor().instrument(engine=engine.sync_engine) | |
| _register_db_span_renaming(engine.sync_engine) | |
| HTTPXClientInstrumentor().instrument() | |
| # unixsocket calls to runners should not be instrumented: every runner | |
| # tunnel has a unique socket path that becomes the net.peer.name metric | |
| # attribute, so the SDK would accumulate state for an unbounded number of series. | |
| RequestsInstrumentor().instrument(excluded_urls=r"http\+unix://") |
- Optionally, move our own
requestscode tohttpx2too, so we have one client for sync and async code, one TLS and proxy model, one exception hierarchy, one OTel instrumentor and one mocking approach. It would also droprequests-unixsocket, since httpx2 supports unix sockets natively.requestsitself stays installed through third-party dependencies (gpuhunt, docker, kubernetes, google/azure SDKs, etc.).
TLS: certifi vs the system trust store
httpx2 no longer uses certifi. For verify=True (2.15.1+):
SSL_CERT_FILE/SSL_CERT_DIRreplace the default store on all OSes.- macOS and Windows use the OS verifier via
truststore(Keychain, Windows store). - Linux loads the system CA files (OpenSSL's compiled-in paths, then the common distro bundle locations) into a stdlib
SSLContextonce. - With no system CA bundle, all HTTPS fails with
CERTIFICATE_VERIFY_FAILED. There is no certifi fallback.
User impact:
- Corporate CAs and TLS-inspecting proxies installed in the OS store start working without configuration.
- Minimal images without
ca-certificatesbreak. Our server image (python:3.11-slim) is fine, but custom images may not be. - macOS verifies more strictly than OpenSSL, so some private or self-signed server certs may be rejected. See Getting SSLError(SSLCertVerificationError('“PRIVATE.CERT” certificate is not standards compliant')) after upgrading to pip 24.2 pypa/pip#12995.
REQUESTS_CA_BUNDLE/CURL_CA_BUNDLEstop applying to anything moved offrequests.SSL_CERT_FILEis the replacement.
Options:
- A. Use the httpx2 default as is.
- B. Pass our own context that matches the httpx2 default but falls back to certifi when the system store is empty, similar to where pip is heading (Switch to using truststore only by default (currently mixed certifi + truststore) pypa/pip#14374). This keeps slim containers working, at the cost of trusting a bundle that only updates with the certifi package.
Python 3.10
httpx2 2.15+ requires Python 3.11, and on 3.10 the resolver stays on 2.14.0, which gets no further fixes. The migration should come with dropping Python 3.10, which depends on #4423.
Behaviour changes when moving off requests (optional part)
- Timeouts: httpx2 defaults to a 5s timeout, while
requestshas none.APIClientsets no timeout today, so long calls would start failing unless we set one explicitly.
dstack/src/dstack/api/server/__init__.py
Lines 180 to 189 in d736e86
| for _ in range(_MAX_RETRIES): | |
| try: | |
| # TODO: set adequate timeout here or everywhere the method is used | |
| resp = self._s.request(method, f"{self._base_url}/{path}", **kwargs) | |
| break | |
| except requests.exceptions.ConnectionError as e: | |
| self._logger.debug("Could not connect to server: %s", e) | |
| time.sleep(_RETRY_INTERVAL) | |
| else: | |
| raise ClientError(f"Failed to connect to dstack server {self._base_url}") |
- Redirects: httpx2 doesn't follow them by default.
APIClienttreats any status below 400 as success, so a 3xx needs explicit handling. - Exceptions:
requests.ConnectionErrorbecomeshttpx2.ConnectError/TransportError, andraise_for_status()also raises on 3xx. Python API users catchingrequestsexceptions around dstack calls would break. - TLS errors and retries: httpcore2 doesn't map every TLS error, so
ssl.SSLErrorcan escape the retry loop. Seessl.SSLErrorduring a request is no longer retried since 3.14.1 openai/openai-python#3969. - API differences:
data=<str>becomescontent=,.okbecomes.is_success,iter_contentbecomesiter_bytes. - Unix sockets:
http+unix://URLs becomeHTTPTransport(uds=...)with a regular base URL. - Tests:
requests_mock(300+ usages) moves tohttpx2.MockTransportor an httpx2 mocking plugin.
Plan
- Drop Python 3.10 ([Feature]: Support Python version upgrade for gateways and migrate from Python 3.10 #4423)
- Decide on TLS policy (A or B)
-
httpx→httpx2in server, proxy, gateway, fluentbit, vastai; switch OTel instrumentor - (optional)
APIClientand CLI →httpx2, with explicit timeouts, redirect handling, and release notes onREQUESTS_CA_BUNDLE→SSL_CERT_FILE - (optional) Runner/shim client and backend API clients →
httpx2 - Test the CLI on macOS with a private-CA server, on uv-managed Python, and in a minimal container
Activity