Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50ed3d9e3a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Re-trigger cubic
There was a problem hiding this comment.
🟠 High
httpx2/src/httpcore2/httpcore2/_sync/http2.py
Line 401 in 7d872bb
A pending SETTINGS_MAX_CONCURRENT_STREAMS=0 is ignored, so the semaphore retains positive capacity and the next request sends HEADERS even though the peer prohibits opening streams; the server can consequently refuse that request. _receive_remote_settings_change skips the update because if new_max_streams is false for zero. Handle zero as a valid limit by testing whether the value differs from _max_streams.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/httpcore2/httpcore2/_sync/http2.py around line 401:
A pending `SETTINGS_MAX_CONCURRENT_STREAMS=0` is ignored, so the semaphore retains positive capacity and the next request sends `HEADERS` even though the peer prohibits opening streams; the server can consequently refuse that request. `_receive_remote_settings_change` skips the update because `if new_max_streams` is false for zero. Handle zero as a valid limit by testing whether the value differs from `_max_streams`.
| while await self._receive_events(request, read_available=True): | ||
| pass |
There was a problem hiding this comment.
🟠 High _async/http2.py:93
The unbounded probe loop can prevent handle_async_request from ever sending the request when an idle peer continuously sends valid frames such as PING; each read returns True, so the loop holds _state_lock indefinitely despite the request timeout. Limit the probe to a bounded read instead of draining until the socket is empty.
- while await self._receive_events(request, read_available=True):
- pass
+ await self._receive_events(request, read_available=True)Also found in 1 other location(s)
src/httpcore2/httpcore2/_sync/http2.py:93
The unbounded drain loop at
while self._receive_events(..., read_available=True)has no elapsed-time or frame limit. An idle HTTP/2 peer that continuously sends valid connection-level frames (for example PINGs) can keep each nonblocking read nonempty indefinitely, sohandle_requestnever reaches the request send and holds_state_lockfor the duration. The new request therefore hangs despite its configured timeout.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/httpcore2/httpcore2/_async/http2.py around lines 93-94:
The unbounded probe loop can prevent `handle_async_request` from ever sending the request when an idle peer continuously sends valid frames such as `PING`; each read returns `True`, so the loop holds `_state_lock` indefinitely despite the request timeout. Limit the probe to a bounded read instead of draining until the socket is empty.
Also found in 1 other location(s):
- src/httpcore2/httpcore2/_sync/http2.py:93 -- The unbounded drain loop at `while self._receive_events(..., read_available=True)` has no elapsed-time or frame limit. An idle HTTP/2 peer that continuously sends valid connection-level frames (for example PINGs) can keep each nonblocking read nonempty indefinitely, so `handle_request` never reaches the request send and holds `_state_lock` for the duration. The new request therefore hangs despite its configured timeout.
Addresses encode/httpx#2983, tracked in #849.
Process pending input before reusing an idle HTTP/2 connection instead of treating socket readability as proof of disconnection.
PINGandSETTINGSframes are handled and acknowledged on the existing connection. When the probe finds EOF,GOAWAY, or an unusable connection, the pool replaces it before sending the next request's headers or consuming its body.The built-in backends expose an optional
read_availablestream capability. It checks TLS buffers as well as transport input, including TLS-in-TLS proxy connections, and preserves incomplete records and frames. Async probes use the configured read timeout for any native I/O; there are no fixed probe delays, background readers, or blind request retries. TLS control output generated during a probe is retained and flushed in wire order during normal I/O.Custom backends without the capability keep their existing behavior. Unsupported custom transport shapes decline the probe without breaking ordinary reads or writes. The AnyIO implementation checks its asyncio transport's buffered-input event because raw-socket readiness alone cannot see that data.
Validation
GOAWAY, malformed control frames, healthyPING/SETTINGSreuse, and partial HTTP/2 and TLS records. Run with sync, asyncio, and Trio clients, directly and through HTTPS proxies, using one-shot request bodies.AI Disclaimer
This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.