fix(agent): replace socket_options transport with httpx pool-level keepalive expiry (#54550 salvage) - #58766
Merged
Conversation
…evel keepalive expiry The custom ``httpx.HTTPTransport(socket_options=[SO_KEEPALIVE, ...])`` in ``_build_keepalive_http_client()`` was introduced to fix CLOSE-WAIT socket accumulation on long-lived connections (#10324). That approach broke streaming for providers behind reverse proxies (OpenResty, Cloudflare, etc.) because the custom socket options conflict with the proxy's chunked-transfer handling (#54049, #12952). It also stripped TCP_NODELAY, stalling TLS handshakes and SSE encoding. Narrow per-provider bypasses were added for Copilot (#50298), Codex (#36623, #12953), but the root cause remained. The fix moves connection lifecycle management from the socket layer to the HTTP pool layer: - ``httpx.Limits(keepalive_expiry=20.0)`` tells httpx to close idle pooled connections at 20 s, before a reverse proxy's typical 30-60 s timeout drops them and causes CLOSE-WAIT accumulation. - The default httpx transport preserves OS TCP defaults (including TCP_NODELAY), so TLS handshakes and SSE chunked encoding work correctly. - ``trust_env=False`` prevents httpx from double-dipping on env vars (we handle proxy detection ourselves via ``_get_proxy_for_base_url`` which respects NO_PROXY). - The Copilot host bypass (line 3632) is no longer needed since all providers now use the same standard httpx.Client. Closes #54049. Supersedes #12010, #36623, #12953, #50298.
…ng builder The salvaged #54550 converted AIAgent._build_keepalive_http_client but the near-identical build_keepalive_http_client in agent/process_bootstrap.py (used by auxiliary clients: compression, vision, web_extract, titles) kept the socket_options transport and the api.githubcopilot.com bypass. Same conversion: httpx.Limits(keepalive_expiry=20) + pool timeouts, verify forwarded on client and no-proxy mounts, copilot hardcode removed.
This was referenced Jul 5, 2026
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Connection lifecycle moves from the socket layer to the HTTP pool layer: the custom
httpx.HTTPTransport(socket_options=[SO_KEEPALIVE, ...])is replaced withhttpx.Limits(keepalive_expiry=20.0), fixing streaming breakage / TLS handshake stalls behind reverse proxies (Cloudflare, OpenResty) for ALL providers and deleting the per-domain bypass list instead of growing it.Salvages #54550 by @DavidMetcalfe onto current main, authorship preserved, widened to the sibling builder in
agent/process_bootstrap.py(aux clients: compression, vision, web_extract, titles) which the original missed. Retires the whole bug class behind #58392 / #36623 / #12953 (per-domain bypass PRs).Why
The socket_options transport was added to stop CLOSE-WAIT accumulation (#10324), but it conflicts with reverse-proxy chunked transfer handling (#54049, #12952) and stripped TCP_NODELAY, stalling TLS handshakes — which is why the copilot hardcoded bypass existed and per-domain bypass PRs kept arriving.
keepalive_expiry=20.0reaps idle pooled connections before a proxy's typical 30-60s timeout drops them — same CLOSE-WAIT protection, right layer, no socket meddling.Changes
run_agent.py:_build_keepalive_http_client→ pool limits + timeouts (read=None for SSE), plain no-proxy mounts to preserve NO_PROXY resolution,verify=kept on client and mounts, copilot hardcode removedagent/process_bootstrap.py: same conversion for the aux-client builder (sync + async), copilot hardcode removedtests/run_agent/test_create_openai_client_proxy_env.py: pinning test updated — proxy mount still asserted, socket_options asserted ABSENTValidation (live, before/after A/B)
Also verified live: keepalive_expiry=20.0 present on the pool and idle connection actually reaped after 22s idle;
verify=Falsehonored +verify=Truerejects self-signed (badssl.com); HTTPS_PROXY routed (dead-proxy fail proves routing) + NO_PROXY loopback bypass preserved; fullhermes chat -qE2E through the worktree completed a real OpenRouter conversation. Targeted suites: 333 tests green (proxy env, auxiliary client, stream timeout floor, attribution headers).Closes #54550. Closes #58392. Closes #36623. Closes #12953.
Infographic