Skip to content

Commit 4a62434

Browse files
committed
fix: configure uvicorn properly for 3.14 and Windows instead of abandoning shutdown
Two root-cause fixes, no warning suppression: Python 3.14: uvicorn's interface='auto' autodetect calls asyncio.iscoroutinefunction on the app's __call__ to distinguish ASGI2 from ASGI3. That function is deprecated on 3.14, and pyproject.toml's filterwarnings=error promotes the warning to an exception in the server thread — which dies silently while our pre-bound socket keeps accepting connections into the kernel queue, turning a loud startup crash into a 5-minute ReadTimeout. Starlette apps are ASGI3. Setting interface='asgi3' skips the autodetect entirely — we're just telling uvicorn something we already know. Windows Proactor: force_exit=True short-circuits _wait_tasks_to_complete's connection-drain loops immediately. connection.shutdown() was called, but we never waited for the transports to actually close; they still have pending Overlapped Recv operations when the event loop is torn down, and GC later finds them during an unrelated test. timeout_graceful_shutdown=1 gives uvicorn a bounded window to drain connections naturally, then on timeout calls t.cancel() on remaining tasks — proper asyncio cancellation that unwinds the transports through their CancelledError paths instead of abandoning them mid-I/O. Tests that close their httpx clients cleanly (all of them, in practice) never hit the timeout; the drain loop finds connections already empty.
1 parent 21a3979 commit 4a62434

File tree

1 file changed

+12
-1
lines changed

1 file changed

+12
-1
lines changed

tests/test_helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def run_uvicorn_in_thread(app: Any, **config_kwargs: Any) -> Generator[str, None
6565
"""
6666
host = config_kwargs.setdefault("host", "127.0.0.1")
6767
config_kwargs.setdefault("log_level", "error")
68+
# Starlette apps are ASGI3. Being explicit skips uvicorn's autodetect,
69+
# which calls asyncio.iscoroutinefunction — deprecated on 3.14 and
70+
# promoted to an error by pyproject.toml's filterwarnings, killing the
71+
# thread before it starts serving.
72+
config_kwargs.setdefault("interface", "asgi3")
73+
# Without a bound, shutdown() waits indefinitely for open connections
74+
# (SSE streams left open at test exit) to drain. force_exit short-circuits
75+
# that wait but abandons connection transports mid-I/O — on Windows
76+
# Proactor those are Overlapped Recv ops that GC later finds still
77+
# pending. timeout_graceful_shutdown waits briefly, then cancels the
78+
# connection tasks via asyncio cancellation so transports unwind cleanly.
79+
config_kwargs.setdefault("timeout_graceful_shutdown", 1)
6880

6981
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
7082
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -81,6 +93,5 @@ def run_uvicorn_in_thread(app: Any, **config_kwargs: Any) -> Generator[str, None
8193
yield f"http://{host}:{port}"
8294
finally:
8395
server.should_exit = True
84-
server.force_exit = True
8596
thread.join(timeout=5)
8697
sock.close()

0 commit comments

Comments
 (0)