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.
The SSE and StreamableHTTP security test files have been flaking in CI. Two recent examples: test_sse_security_custom_allowed_hosts failing with 421 for an explicitly allowed host, and test_idle_session_is_reaped failing 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 allows custom.host observes 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 from tests.interaction.transports as 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 return in _handle_get_request, so the now-stale # pragma: no cover on that line is removed (the success path was already driven in process by the interaction suite).
Two subtleties worth knowing as a reviewer:
tests/shared/test_sse.py, tests/shared/test_streamable_http.py, and tests/client/test_http_unicode.py still use the subprocess pattern and are follow-up candidates.
How Has This Been Tested?
Breaking 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