diff --git a/pyproject.toml b/pyproject.toml index 3ce30a7..a64f17d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,13 @@ classifiers = [ dependencies = [ "fastapi>=0.136,<1", "uvicorn>=0.32,<1", + # The /track-ws endpoint needs a WebSocket protocol backend at runtime. + # Bare `uvicorn` ships none (it lives in the `[standard]` extra), so under + # gunicorn's UvicornWorker every upgrade 404s with "No supported WebSocket + # library detected". Depend on `websockets` directly — lighter than the + # full `[standard]` extra (no uvloop/httptools/watchfiles) and all the + # worker actually needs to accept the upgrade. + "websockets>=14,<17", "gunicorn>=23.0,<24", "httpx>=0.28,<1", "tenacity>=9.0,<10", diff --git a/tests/test_app.py b/tests/test_app.py index b7556de..869afd0 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -196,3 +196,22 @@ async def receive(): # type: ignore[no-untyped-def] assert starts and starts[0]["status"] == 403 # App was never called because the middleware short-circuited. assert ("app", GATED_PATH) not in received + + +def test_uvicorn_has_websocket_backend() -> None: + """Guard the /track-ws upgrade against a missing WebSocket backend. + + Under gunicorn's ``UvicornWorker`` the WebSocket upgrade only works if a + protocol library (``websockets`` or ``wsproto``) is installed. Bare + ``uvicorn`` ships neither — they live in the ``uvicorn[standard]`` extra — + so without an explicit dependency the env builds fine, tests pass via + Starlette's in-process TestClient, and yet every live ``/track-ws`` 404s + with "No supported WebSocket library detected". This asserts the backend + is present so that regression can't ship silently again. + """ + import importlib.util + + assert importlib.util.find_spec("websockets") or importlib.util.find_spec("wsproto"), ( + "no WebSocket backend (websockets/wsproto) installed — /track-ws will 404 " + "under uvicorn; add `websockets` to project dependencies" + ) diff --git a/uv.lock b/uv.lock index f4a98bb..b7b258f 100644 --- a/uv.lock +++ b/uv.lock @@ -642,6 +642,7 @@ dependencies = [ { name = "tenacity" }, { name = "uvicorn" }, { name = "weasyprint" }, + { name = "websockets" }, ] [package.dev-dependencies] @@ -673,6 +674,7 @@ requires-dist = [ { name = "tenacity", specifier = ">=9.0,<10" }, { name = "uvicorn", specifier = ">=0.32,<1" }, { name = "weasyprint", specifier = ">=63.0,<67" }, + { name = "websockets", specifier = ">=14,<17" }, ] [package.metadata.requires-dev]