Skip to content

Commit b960626

Browse files
authored
Add WebSocketsSansIOProtocol (#2540)
1 parent 5432729 commit b960626

File tree

9 files changed

+448
-21
lines changed

9 files changed

+448
-21
lines changed

docs/settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Using Uvicorn with watchfiles will enable the following options (which are other
9191

9292
* `--loop <str>` - Set the event loop implementation. The uvloop implementation provides greater performance, but is not compatible with Windows or PyPy. **Options:** *'auto', 'asyncio', 'uvloop'.* **Default:** *'auto'*.
9393
* `--http <str>` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy. **Options:** *'auto', 'h11', 'httptools'.* **Default:** *'auto'*.
94-
* `--ws <str>` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. Use `'none'` to ignore all websocket requests. **Options:** *'auto', 'none', 'websockets', 'wsproto'.* **Default:** *'auto'*.
94+
* `--ws <str>` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. There are two versions of `websockets` supported: `websockets` and `websockets-sansio`. Use `'none'` to ignore all websocket requests. **Options:** *'auto', 'none', 'websockets', 'websockets-sansio', 'wsproto'.* **Default:** *'auto'*.
9595
* `--ws-max-size <int>` - Set the WebSockets max message size, in bytes. Only available with the `websockets` protocol. **Default:** *16777216* (16 MB).
9696
* `--ws-max-queue <int>` - Set the maximum length of the WebSocket incoming message queue. Only available with the `websockets` protocol. **Default:** *32*.
9797
* `--ws-ping-interval <float>` - Set the WebSockets ping interval, in seconds. Only available with the `websockets` protocol. **Default:** *20.0*.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ filterwarnings = [
9292
"ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
9393
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
9494
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
95+
"ignore: websockets.legacy is deprecated.*:DeprecationWarning",
96+
"ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning",
97+
"ignore: websockets.client.connect is deprecated.*:DeprecationWarning",
9598
]
9699

97100
[tool.coverage.run]

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,9 @@ def unused_tcp_port() -> int:
233233
marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
234234
id="wsproto",
235235
),
236+
pytest.param("uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", id="websockets"),
236237
pytest.param(
237-
"uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
238-
id="websockets",
238+
"uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio"
239239
),
240240
]
241241
)

tests/middleware/test_logging.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
import socket
66
import sys
77
from collections.abc import Iterator
8-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, Any
99

1010
import httpx
1111
import pytest
12-
import websockets
1312
import websockets.client
13+
from websockets.protocol import State
1414

1515
from tests.utils import run_server
1616
from uvicorn import Config
@@ -50,7 +50,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
5050
await send({"type": "http.response.body", "body": b"", "more_body": False})
5151

5252

53-
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
53+
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int):
5454
config = Config(
5555
app=app,
5656
log_level="trace",
@@ -92,8 +92,8 @@ async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging
9292

9393
async def test_trace_logging_on_ws_protocol(
9494
ws_protocol_cls: WSProtocol,
95-
caplog,
96-
logging_config,
95+
caplog: pytest.LogCaptureFixture,
96+
logging_config: dict[str, Any],
9797
unused_tcp_port: int,
9898
):
9999
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@@ -105,9 +105,9 @@ async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISe
105105
elif message["type"] == "websocket.disconnect":
106106
break
107107

108-
async def open_connection(url):
108+
async def open_connection(url: str):
109109
async with websockets.client.connect(url) as websocket:
110-
return websocket.open
110+
return websocket.state is State.OPEN
111111

112112
config = Config(
113113
app=websocket_app,
@@ -127,7 +127,9 @@ async def open_connection(url):
127127

128128

129129
@pytest.mark.parametrize("use_colors", [(True), (False), (None)])
130-
async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
130+
async def test_access_logging(
131+
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
132+
):
131133
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
132134
with caplog_for_logger(caplog, "uvicorn.access"):
133135
async with run_server(config):
@@ -141,7 +143,7 @@ async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture
141143

142144
@pytest.mark.parametrize("use_colors", [(True), (False)])
143145
async def test_default_logging(
144-
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int
146+
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
145147
):
146148
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
147149
with caplog_for_logger(caplog, "uvicorn.access"):

tests/middleware/test_proxy_headers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISe
465465
host, port = scope["client"]
466466
await send({"type": "websocket.accept"})
467467
await send({"type": "websocket.send", "text": f"{scheme}://{host}:{port}"})
468+
await send({"type": "websocket.close"})
468469

469470
app_with_middleware = ProxyHeadersMiddleware(websocket_app, trusted_hosts="*")
470471
config = Config(

tests/protocols/test_websocket.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -597,12 +597,9 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
597597
await send_accept_task.wait()
598598
disconnect_message = await receive() # type: ignore
599599

600-
response: httpx.Response | None = None
601-
602600
async def websocket_session(uri: str):
603-
nonlocal response
604601
async with httpx.AsyncClient() as client:
605-
response = await client.get(
602+
await client.get(
606603
f"http://127.0.0.1:{unused_tcp_port}",
607604
headers={
608605
"upgrade": "websocket",
@@ -619,9 +616,6 @@ async def websocket_session(uri: str):
619616
send_accept_task.set()
620617
await asyncio.sleep(0.1)
621618

622-
assert response is not None
623-
assert response.status_code == 500, response.text
624-
assert response.text == "Internal Server Error"
625619
assert disconnect_message == {"type": "websocket.disconnect", "code": 1006}
626620
await task
627621

@@ -916,6 +910,9 @@ async def websocket_session(url: str):
916910
async def test_server_reject_connection_with_invalid_msg(
917911
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
918912
):
913+
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
914+
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
915+
919916
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
920917
assert scope["type"] == "websocket"
921918
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@@ -947,6 +944,9 @@ async def websocket_session(url: str):
947944
async def test_server_reject_connection_with_missing_body(
948945
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
949946
):
947+
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
948+
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
949+
950950
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
951951
assert scope["type"] == "websocket"
952952
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@@ -982,6 +982,8 @@ async def test_server_multiple_websocket_http_response_start_events(
982982
The server should raise an exception if it sends multiple
983983
websocket.http.response.start events.
984984
"""
985+
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
986+
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
985987
exception_message: str | None = None
986988

987989
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):

uvicorn/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from uvicorn.middleware.wsgi import WSGIMiddleware
2626

2727
HTTPProtocolType = Literal["auto", "h11", "httptools"]
28-
WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
28+
WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
2929
LifespanType = Literal["auto", "on", "off"]
3030
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
3131
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
@@ -47,6 +47,7 @@
4747
"auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
4848
"none": None,
4949
"websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
50+
"websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol",
5051
"wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
5152
}
5253
LIFESPAN: dict[LifespanType, str] = {

0 commit comments

Comments
 (0)