From ad5ff87c869e8a34e9b04fcd5ca38d65c526893c Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Thu, 30 Apr 2026 14:26:13 -0400 Subject: [PATCH] Treat `fd=0` as a valid file descriptor with reload/workers (#2927) Co-authored-by: Marcelo Trylesinski --- tests/test_config.py | 31 +++++++++++++++++++++++++++++++ tests/test_server.py | 18 +++++++----------- uvicorn/config.py | 4 ++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index db758662..f330dea3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -553,6 +553,37 @@ def test_bind_fd_works_with_reload_or_workers(reload: bool, workers: int): # pr fdsock.close() +@pytest.fixture +def stdin_socket() -> Iterator[socket.socket]: # pragma: py-win32 + with closing(socket.socket(socket.AF_INET)) as sock: + sock.bind(("127.0.0.1", 0)) + saved_stdin = os.dup(0) + os.dup2(sock.fileno(), 0) + try: + yield sock + finally: + os.dup2(saved_stdin, 0) + os.close(saved_stdin) + + +@pytest.mark.parametrize( + "reload, workers", + [ + (True, 1), + (False, 2), + ], + ids=["--reload=True --workers=1", "--reload=False --workers=2"], +) +@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system") +def test_bind_stdin_works_with_reload_or_workers( + reload: bool, workers: int, stdin_socket: socket.socket +): # pragma: py-win32 + config = Config(app=asgi_app, fd=0, reload=reload, workers=workers) + config.load() + with closing(config.bind_socket()) as sock: + assert sock.getsockname() == stdin_socket.getsockname() + + @pytest.mark.parametrize( "reload, workers, expected", [ diff --git a/tests/test_server.py b/tests/test_server.py index f57c1f23..b6f56084 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -142,17 +142,13 @@ async def test_limit_max_requests_jitter( config = Config( app=app, limit_max_requests=1, limit_max_requests_jitter=2, port=unused_tcp_port, http=http_protocol_cls ) - server = Server(config=config) - limit = server.limit_max_requests - assert limit is not None - assert 1 <= limit <= 3 - task = asyncio.create_task(server.serve()) - while not server.started: - await asyncio.sleep(0.01) - async with httpx.AsyncClient() as client: - for _ in range(limit + 1): - await client.get(f"http://127.0.0.1:{unused_tcp_port}") - await task + async with run_server(config) as server: + limit = server.limit_max_requests + assert limit is not None + assert 1 <= limit <= 3 + async with httpx.AsyncClient() as client: + tasks = [client.get(f"http://127.0.0.1:{unused_tcp_port}") for _ in range(limit + 1)] + await asyncio.gather(*tasks) assert f"Maximum request limit of {limit} exceeded. Terminating process." in caplog.text diff --git a/uvicorn/config.py b/uvicorn/config.py index 84bf9a20..34d04e47 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -537,7 +537,7 @@ class Config: def bind_socket(self) -> socket.socket: logger_args: list[str | int] - if self.uds: # pragma: py-win32 + if self.uds is not None: # pragma: py-win32 path = self.uds sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: @@ -552,7 +552,7 @@ class Config: sock_name_format = "%s" color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)" logger_args = [self.uds] - elif self.fd: # pragma: py-win32 + elif self.fd is not None: # pragma: py-win32 sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM) message = "Uvicorn running on socket %s (Press CTRL+C to quit)" fd_name_format = "%s"