fix: eliminate test port allocation race by running uvicorn in-thread#2263
Draft
fix: eliminate test port allocation race by running uvicorn in-thread#2263
Conversation
The previous pattern picked a free port via socket.bind(0), released it, then started a uvicorn subprocess hoping to rebind — a TOCTOU race that caused intermittent CI failures when pytest-xdist workers stole the port between release and rebind (connection errors, 404s against wrong server, WS 403s). Replaced with a context manager that runs uvicorn in a background thread with port=0 and reads the actual bound port back from the server's socket after startup. The OS atomically assigns the port at bind time and the server holds it until shutdown — no race window. Also fixed a latent stream leak in SseServerTransport.connect_sse() that the in-process server surfaced: sse_stream_reader was never closed on normal completion or cancellation (sse_starlette only closes it on SendTimeout). Cleanup now runs in a finally block. Side benefits: no more subprocess spawn overhead, faster startup, shared process state when needed, and wait_for_server() is no longer used.
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
Replaces the subprocess + port-race test fixture pattern with an in-thread uvicorn helper that uses
port=0, eliminating the TOCTOU race that caused intermittent CI failures under pytest-xdist.The Problem
Nine locations across seven test files used this pattern:
Between
getsockname()returning and the subprocess rebinding, another pytest-xdist worker can claim the port. This caused:httpx.ConnectError: All connection attempts failed(job 63508538456)404 Not Foundwhen connecting to the wrong server (#1573, #1776)403WS rejection when the port was bound by a non-WS server (job 63414508256)PR #1528 attempted worker-specific port ranges but was closed as only reducing (not eliminating) collision probability.
The Fix
tests/test_helpers.pynow providesrun_uvicorn_in_thread(app)— a context manager that:port=0server.started=True(set at the end ofServer.startup(), after sockets are bound and lifespan has started)server.servers[0].sockets[0].getsockname()should_exit=Trueand joins the threadThis is race-free: the OS atomically assigns the port at bind time and the server holds it from that moment until shutdown. No TOCTOU window.
All seven test files were migrated to this pattern. The
*_portfixtures,run_server()subprocess entrypoints, andmultiprocessing.Process+proc.kill()boilerplate are gone.wait_for_server()now has zero callers.Bonus: SSE Stream Leak
Running the server in-process surfaced a pre-existing resource leak in
SseServerTransport.connect_sse()—sse_stream_readerwas never closed becausesse_starlette.EventSourceResponseonly closes its body iterator onSendTimeout, not on normal completion or cancellation. Previously invisible becauseproc.kill()never runs destructors.Fixed by moving the cleanup into a
finallyblock and adding an explicitsse_stream_reader.aclose().Results
test_sse.pyruntimeAI Disclaimer