Run transport security tests in process instead of over sockets#2764
Merged
Conversation
The SSE and StreamableHTTP security tests each spawned a uvicorn subprocess on a port picked by bind-then-close, then polled until the port accepted connections. Under pytest-xdist two workers can pick the same port in that window: the second server fails to bind, the readiness poll succeeds against the other worker's server, and the test asserts against a server configured with different security settings (e.g. 421 for a host the test explicitly allowed). Rewrite both files to drive the same Starlette apps in process through the interaction suite's StreamingASGITransport (re-exported from tests.interaction.transports as the sanctioned import point): no sockets, no subprocesses, no ports to race over. Assertions are unchanged. The new in-process GET test covers the validation-failure return in _handle_get_request; the pragma on that line was already stale (the success path has been driven in process by the interaction suite since it merged) and is removed. Also deflake test_idle_session_is_reaped, which slept 0.1s after a 0.05s idle timeout and failed on slow runners when the reaper had not fired yet. Re-requesting the session to poll for the 404 would push its idle deadline forward, so instead wait on the manager's "idle timeout" log record, which is emitted synchronously with the session being unregistered.
Kludex
approved these changes
Jun 2, 2026
This was referenced 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.
The SSE and StreamableHTTP security test files have been flaking in CI. Two recent examples:
test_sse_security_custom_allowed_hostsfailing with 421 for an explicitly allowed host, andtest_idle_session_is_reapedfailing with 406 instead of 404 on Windows.Motivation and Context
The security tests had a port-allocation race. Each test picked an ephemeral port by bind-then-close, spawned a uvicorn subprocess to re-bind it, and polled until the port accepted connections. Under pytest-xdist, two workers can pick the same port in that window: the second server fails to bind (silently —
log_level="error"in a subprocess), the readiness poll succeeds against the other worker's server, and the test asserts against a server configured with different security settings. That's how a test that allowscustom.hostobserves a 421.The fix removes the network entirely: both files now drive the same Starlette apps in process through the interaction suite's
StreamingASGITransport(re-exported fromtests.interaction.transportsas the sanctioned import point for code outside that suite). No sockets, no subprocesses, no ports to race over. Assertions are unchanged — the diff is a harness swap. Each file also drops from seconds of subprocess churn to ~0.25s.The idle-reap test had a timing race. It slept 0.1s after a 0.05s idle timeout and asserted the session was gone — on a slow runner the reaper hasn't fired yet, the request routes into the still-live transport, and the missing Accept header yields the 406. Polling for the 404 instead would never converge: each request to a live session pushes its idle deadline forward. The test now waits (bounded by
anyio.fail_after(5)) on the manager's own "idle timeout" log record, which is emitted synchronously with the session being unregistered — once observed, the 404 is guaranteed.One src change: the new in-process GET test covers the validation-failure
returnin_handle_get_request, so the now-stale# pragma: no coveron that line is removed (the success path was already driven in process by the interaction suite).Two subtleties worth knowing as a reviewer:
http.response.startthrough the bridge (connect_ssesends the 421/403 itself, then the handler's trailingResponse()is sent by Starlette). This is deterministic because anyio'sMemoryObjectSendStream.send()checkpoints on the non-empty error body before the overwrite can happen (holds down to theanyio>=4.9floor); there's a comment at the spot documenting the assumption.cancel_on_close=Falseso the bridge drains the app's disconnect handling instead of cancelling it, matching how the interaction suite's own SSE leg uses the bridge.tests/shared/test_sse.py,tests/shared/test_streamable_http.py, andtests/client/test_http_unicode.pystill use the subprocess pattern and are follow-up candidates.How Has This Been Tested?
./scripts/testgreen: 1526 passed, 100% line+branch coverage,strict-no-covercleanpytest-xdistat several-nvalues, shuffled test orders, and under sustained CPU load — zero failuresBreaking Changes
None — test-only, plus one pragma removal in src.
Types of changes
Checklist
Additional context
Test names keep their existing feature-label style (rather than the interaction suite's behaviour-sentence names) so the "assertions unchanged" claim stays auditable in the diff; happy to rename in a follow-up if preferred.
AI Disclaimer