Move HTTP/2 stream events cleanup inside _state_lock#1013
Merged
Conversation
`_response_closed` deletes `self._events[stream_id]` *outside* the `_state_lock`, but the `not self._events` check that decides whether to transition the connection to IDLE happens *inside* it. With multiplexed HTTP/2 requests this creates a race: 1. Request A is the last active stream; `_response_closed` releases the semaphore, deletes its events entry, then yields before acquiring `_state_lock`. 2. Request B enters `handle_async_request`, acquires `_state_lock`, sets `state=ACTIVE`, releases the lock. 3. Request B acquires the semaphore and stream ID but has not yet added `self._events[stream_id] = []`. 4. Request A resumes, acquires `_state_lock`, sees `not self._events` is True, and marks the connection IDLE with an expiry — while Request B is actively using it. Moving `del self._events[stream_id]` inside the lock makes the events mutation and the IDLE-transition check atomic. Ports encode/httpcore#1062 (bysiber), via codeberg.org/httpxyz/httpcorexyz@e88f30b (second of two fixes bundled in that commit; the first landed as pydantic#1012). Co-Authored-By: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com>
Merging this PR will not alter performance
Comparing Footnotes
|
mbeijen
added a commit
to mbeijen/httpx2
that referenced
this pull request
Jun 2, 2026
…om fork's e88f30b)
Kludex
approved these changes
Jun 2, 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
In
AsyncHTTP2Connection._response_closed, thedel self._events[stream_id]happens outside the_state_lock, but thenot self._eventscheck that decides whether to transition the connection to IDLE happens inside the lock. With multiplexed HTTP/2 requests this creates a race:_response_closedruns: releases the semaphore, deletes its events entry, then yields before acquiring the lock.handle_async_request, acquires_state_lock, setsstate=ACTIVE, releases the lock.self._events[stream_id] = []._state_lock, observesnot self._eventsis True (B's entry isn't there yet), and marks the connection IDLE with an expiry timer — while Request B is actively using it.The pool may then see the connection as expired and close it mid-request, or evict it as an excess idle connection.
Moving
del self._events[stream_id]inside_state_lockmakes the events-dict mutation and the IDLE-transition check atomic.Ports encode/httpcore#1062 (bysiber, currently open upstream), via codeberg.org/httpxyz/httpcorexyz@e88f30b. The fork bundled this with encode/httpcore#1061; I'm splitting them into two httpx2 PRs to mirror the upstream split. Sibling PR for #1061 is #1012.
Notes
_async/http2.pywas edited by hand;_sync/http2.pywas regenerated byscripts/unasync.py.AsyncConnectionPoolAPI. The upstream PR doesn't ship a test either. 100% coverage preserved; existing HTTP/2 tests continue to pass.Note: this change was prepared with AI assistance (Claude Code).