Summary
httpcore's HTTP/2 implementation uses the h2 library for HTTP/2 framing but never sends PING frames. This causes connections to be silently killed by infrastructure components (load balancers, proxies, CDNs) that enforce idle timeouts on TCP connections.
HTTP/2 PING frames (RFC 9113 §6.7) exist precisely for this purpose — they generate application-layer traffic that resets idle timers. The h2 library already supports conn.ping(), but httpcore never calls it.
Problem
Many network intermediaries enforce idle timeouts on TCP connections. For example, AWS Global Accelerator enforces a 340-second idle timeout that cannot be configured. TCP keepalive packets are explicitly excluded from resetting this timer — only application-layer data counts.
When a client makes a long-running HTTP/2 request (e.g., a request where the server takes several minutes to produce a response), the connection is killed mid-flight:
httpcore.RemoteProtocolError: Server disconnected
This affects any httpx-based client making long-running requests through infrastructure with idle timeouts.
Reproduction
import httpx
# HTTP/2 is negotiated via ALPN, but no PING frames are ever sent.
# If the server takes longer than the intermediary's idle timeout,
# the connection is dropped.
client = httpx.Client(http2=True, timeout=httpx.Timeout(900.0))
resp = client.post("https://example.com/long-running-endpoint", json={...})
# Raises RemoteProtocolError at the idle timeout boundary
Evidence
I tested this against a server with a configurable response delay of 420s, behind an intermediary with a 340s idle timeout:
| Client |
HTTP/2 |
PING frames |
Result |
httpx (http2=True) |
Yes (via ALPN) |
None sent |
FAIL at 340s — Server disconnected |
httpx (http2=False) |
No (HTTP/1.1) |
N/A |
FAIL at 340s — Server disconnected |
Raw h2 library with conn.ping() every 60s |
Yes |
Sent every 60s |
PASS — 200 OK at 421s |
The h2 library's ping() method works correctly — httpcore just never calls it.
Proposed change
Add an optional h2_ping_interval parameter to HTTP2Connection (and surface it through httpx.HTTPTransport / httpx.Client). When set, a background thread sends PING frames at the specified interval on active connections.
The implementation is small. In HTTP2Connection:
- Accept
h2_ping_interval: float | None = None in __init__
- After
_send_connection_init, start a daemon thread that calls self._h2_state.ping() + self._write_outgoing_data() every h2_ping_interval seconds
- Stop the thread in
close()
Default would be None (current behavior, no PINGs), so this is fully backwards-compatible.
References
- RFC 9113 §6.7 — PING: "PING frames are a mechanism for measuring a minimal round-trip time from the sender, as well as determining whether an idle connection is still functional."
- AWS Global Accelerator idle timeout: 340s, not configurable, TCP keepalives excluded
- The
h2 library already supports H2Connection.ping() — httpcore just needs to call it
Summary
httpcore's HTTP/2 implementation uses the
h2library for HTTP/2 framing but never sends PING frames. This causes connections to be silently killed by infrastructure components (load balancers, proxies, CDNs) that enforce idle timeouts on TCP connections.HTTP/2 PING frames (RFC 9113 §6.7) exist precisely for this purpose — they generate application-layer traffic that resets idle timers. The
h2library already supportsconn.ping(), but httpcore never calls it.Problem
Many network intermediaries enforce idle timeouts on TCP connections. For example, AWS Global Accelerator enforces a 340-second idle timeout that cannot be configured. TCP keepalive packets are explicitly excluded from resetting this timer — only application-layer data counts.
When a client makes a long-running HTTP/2 request (e.g., a request where the server takes several minutes to produce a response), the connection is killed mid-flight:
This affects any
httpx-based client making long-running requests through infrastructure with idle timeouts.Reproduction
Evidence
I tested this against a server with a configurable response delay of 420s, behind an intermediary with a 340s idle timeout:
httpx(http2=True)Server disconnectedhttpx(http2=False)Server disconnectedh2library withconn.ping()every 60sThe
h2library'sping()method works correctly — httpcore just never calls it.Proposed change
Add an optional
h2_ping_intervalparameter toHTTP2Connection(and surface it throughhttpx.HTTPTransport/httpx.Client). When set, a background thread sends PING frames at the specified interval on active connections.The implementation is small. In
HTTP2Connection:h2_ping_interval: float | None = Nonein__init___send_connection_init, start a daemon thread that callsself._h2_state.ping()+self._write_outgoing_data()everyh2_ping_intervalsecondsclose()Default would be
None(current behavior, no PINGs), so this is fully backwards-compatible.References
h2library already supportsH2Connection.ping()— httpcore just needs to call it