From bdf770d7d728d0f9071f5857ae96ae0663398ef9 Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Sun, 5 Jan 2025 11:25:30 -0500 Subject: [PATCH] ilot/uvicorn: new aport --- .../2540_add-websocketssansioprotocol.patch | 618 ++++++++++++++++++ .../2541_bump-wesockets-on-requirements.patch | 567 ++++++++++++++++ ilot/uvicorn/APKBUILD | 59 ++ ilot/uvicorn/fix-test-wsgi.patch | 13 + ilot/uvicorn/test_multiprocess.patch | 14 + 5 files changed, 1271 insertions(+) create mode 100644 ilot/uvicorn/2540_add-websocketssansioprotocol.patch create mode 100644 ilot/uvicorn/2541_bump-wesockets-on-requirements.patch create mode 100644 ilot/uvicorn/APKBUILD create mode 100644 ilot/uvicorn/fix-test-wsgi.patch create mode 100644 ilot/uvicorn/test_multiprocess.patch diff --git a/ilot/uvicorn/2540_add-websocketssansioprotocol.patch b/ilot/uvicorn/2540_add-websocketssansioprotocol.patch new file mode 100644 index 0000000..0cb8db4 --- /dev/null +++ b/ilot/uvicorn/2540_add-websocketssansioprotocol.patch @@ -0,0 +1,618 @@ +diff --git a/docs/deployment.md b/docs/deployment.md +index d69fcf8..99dfbf3 100644 +--- a/docs/deployment.md ++++ b/docs/deployment.md +@@ -60,7 +60,7 @@ Options: + --loop [auto|asyncio|uvloop] Event loop implementation. [default: auto] + --http [auto|h11|httptools] HTTP protocol implementation. [default: + auto] +- --ws [auto|none|websockets|wsproto] ++ --ws [auto|none|websockets|websockets-sansio|wsproto] + WebSocket protocol implementation. + [default: auto] + --ws-max-size INTEGER WebSocket max size message in bytes +diff --git a/docs/index.md b/docs/index.md +index bb6fc32..50e2ab9 100644 +--- a/docs/index.md ++++ b/docs/index.md +@@ -130,7 +130,7 @@ Options: + --loop [auto|asyncio|uvloop] Event loop implementation. [default: auto] + --http [auto|h11|httptools] HTTP protocol implementation. [default: + auto] +- --ws [auto|none|websockets|wsproto] ++ --ws [auto|none|websockets|websockets-sansio|wsproto] + WebSocket protocol implementation. + [default: auto] + --ws-max-size INTEGER WebSocket max size message in bytes +diff --git a/pyproject.toml b/pyproject.toml +index 0a89966..8771bfb 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -92,6 +92,10 @@ filterwarnings = [ + "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning", + "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning", + "ignore: remove second argument of ws_handler:DeprecationWarning:websockets", ++ "ignore: websockets.legacy is deprecated.*:DeprecationWarning", ++ "ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning", ++ "ignore: websockets.client.connect is deprecated.*:DeprecationWarning", ++ "ignore: websockets.exceptions.InvalidStatusCode is deprecated", + ] + + [tool.coverage.run] +diff --git a/tests/conftest.py b/tests/conftest.py +index 1b0c0e8..7061a14 100644 +--- a/tests/conftest.py ++++ b/tests/conftest.py +@@ -233,9 +233,9 @@ def unused_tcp_port() -> int: + marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."), + id="wsproto", + ), ++ pytest.param("uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", id="websockets"), + pytest.param( +- "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", +- id="websockets", ++ "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio" + ), + ] + ) +diff --git a/tests/middleware/test_logging.py b/tests/middleware/test_logging.py +index f27633a..63d7daf 100644 +--- a/tests/middleware/test_logging.py ++++ b/tests/middleware/test_logging.py +@@ -49,7 +49,9 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable + await send({"type": "http.response.body", "body": b"", "more_body": False}) + + +-async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int): ++async def test_trace_logging( ++ caplog: pytest.LogCaptureFixture, logging_config: dict[str, typing.Any], unused_tcp_port: int ++): + config = Config( + app=app, + log_level="trace", +@@ -91,8 +93,8 @@ async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging + + async def test_trace_logging_on_ws_protocol( + ws_protocol_cls: WSProtocol, +- caplog, +- logging_config, ++ caplog: pytest.LogCaptureFixture, ++ logging_config: dict[str, typing.Any], + unused_tcp_port: int, + ): + async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): +@@ -104,7 +106,7 @@ async def test_trace_logging_on_ws_protocol( + elif message["type"] == "websocket.disconnect": + break + +- async def open_connection(url): ++ async def open_connection(url: str): + async with websockets.client.connect(url) as websocket: + return websocket.open + +diff --git a/tests/middleware/test_proxy_headers.py b/tests/middleware/test_proxy_headers.py +index 0ade974..d300c45 100644 +--- a/tests/middleware/test_proxy_headers.py ++++ b/tests/middleware/test_proxy_headers.py +@@ -465,6 +465,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto( + host, port = scope["client"] + await send({"type": "websocket.accept"}) + await send({"type": "websocket.send", "text": f"{scheme}://{host}:{port}"}) ++ await send({"type": "websocket.close"}) + + app_with_middleware = ProxyHeadersMiddleware(websocket_app, trusted_hosts="*") + config = Config( +diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py +index 15ccfdd..e728544 100644 +--- a/tests/protocols/test_websocket.py ++++ b/tests/protocols/test_websocket.py +@@ -7,6 +7,8 @@ from copy import deepcopy + import httpx + import pytest + import websockets ++import websockets.asyncio ++import websockets.asyncio.client + import websockets.client + import websockets.exceptions + from typing_extensions import TypedDict +@@ -601,12 +603,9 @@ async def test_connection_lost_before_handshake_complete( + await send_accept_task.wait() + disconnect_message = await receive() # type: ignore + +- response: httpx.Response | None = None +- + async def websocket_session(uri: str): +- nonlocal response + async with httpx.AsyncClient() as client: +- response = await client.get( ++ await client.get( + f"http://127.0.0.1:{unused_tcp_port}", + headers={ + "upgrade": "websocket", +@@ -623,9 +622,6 @@ async def test_connection_lost_before_handshake_complete( + send_accept_task.set() + await asyncio.sleep(0.1) + +- assert response is not None +- assert response.status_code == 500, response.text +- assert response.text == "Internal Server Error" + assert disconnect_message == {"type": "websocket.disconnect", "code": 1006} + await task + +@@ -920,6 +916,9 @@ async def test_server_reject_connection_with_body_nolength( + async def test_server_reject_connection_with_invalid_msg( + ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int + ): ++ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol": ++ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.") ++ + async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): + assert scope["type"] == "websocket" + assert "extensions" in scope and "websocket.http.response" in scope["extensions"] +@@ -951,6 +950,9 @@ async def test_server_reject_connection_with_invalid_msg( + async def test_server_reject_connection_with_missing_body( + ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int + ): ++ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol": ++ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.") ++ + async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): + assert scope["type"] == "websocket" + assert "extensions" in scope and "websocket.http.response" in scope["extensions"] +@@ -986,6 +988,8 @@ async def test_server_multiple_websocket_http_response_start_events( + The server should raise an exception if it sends multiple + websocket.http.response.start events. + """ ++ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol": ++ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.") + exception_message: str | None = None + + async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): +diff --git a/uvicorn/config.py b/uvicorn/config.py +index 664d191..cbfeea6 100644 +--- a/uvicorn/config.py ++++ b/uvicorn/config.py +@@ -25,7 +25,7 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware + from uvicorn.middleware.wsgi import WSGIMiddleware + + HTTPProtocolType = Literal["auto", "h11", "httptools"] +-WSProtocolType = Literal["auto", "none", "websockets", "wsproto"] ++WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"] + LifespanType = Literal["auto", "on", "off"] + LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"] + InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"] +@@ -47,6 +47,7 @@ WS_PROTOCOLS: dict[WSProtocolType, str | None] = { + "auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol", + "none": None, + "websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", ++ "websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", + "wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol", + } + LIFESPAN: dict[LifespanType, str] = { +diff --git a/uvicorn/protocols/websockets/websockets_sansio_impl.py b/uvicorn/protocols/websockets/websockets_sansio_impl.py +new file mode 100644 +index 0000000..994af07 +--- /dev/null ++++ b/uvicorn/protocols/websockets/websockets_sansio_impl.py +@@ -0,0 +1,405 @@ ++from __future__ import annotations ++ ++import asyncio ++import logging ++from asyncio.transports import BaseTransport, Transport ++from http import HTTPStatus ++from typing import Any, Literal, cast ++from urllib.parse import unquote ++ ++from websockets import InvalidState ++from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory ++from websockets.frames import Frame, Opcode ++from websockets.http11 import Request ++from websockets.server import ServerProtocol ++ ++from uvicorn._types import ( ++ ASGIReceiveEvent, ++ ASGISendEvent, ++ WebSocketAcceptEvent, ++ WebSocketCloseEvent, ++ WebSocketDisconnectEvent, ++ WebSocketReceiveEvent, ++ WebSocketResponseBodyEvent, ++ WebSocketResponseStartEvent, ++ WebSocketScope, ++ WebSocketSendEvent, ++) ++from uvicorn.config import Config ++from uvicorn.logging import TRACE_LOG_LEVEL ++from uvicorn.protocols.utils import ( ++ ClientDisconnected, ++ get_local_addr, ++ get_path_with_query_string, ++ get_remote_addr, ++ is_ssl, ++) ++from uvicorn.server import ServerState ++ ++ ++class WebSocketsSansIOProtocol(asyncio.Protocol): ++ def __init__( ++ self, ++ config: Config, ++ server_state: ServerState, ++ app_state: dict[str, Any], ++ _loop: asyncio.AbstractEventLoop | None = None, ++ ) -> None: ++ if not config.loaded: ++ config.load() # pragma: no cover ++ ++ self.config = config ++ self.app = config.loaded_app ++ self.loop = _loop or asyncio.get_event_loop() ++ self.logger = logging.getLogger("uvicorn.error") ++ self.root_path = config.root_path ++ self.app_state = app_state ++ ++ # Shared server state ++ self.connections = server_state.connections ++ self.tasks = server_state.tasks ++ self.default_headers = server_state.default_headers ++ ++ # Connection state ++ self.transport: asyncio.Transport = None # type: ignore[assignment] ++ self.server: tuple[str, int] | None = None ++ self.client: tuple[str, int] | None = None ++ self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment] ++ ++ # WebSocket state ++ self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue() ++ self.handshake_initiated = False ++ self.handshake_complete = False ++ self.close_sent = False ++ self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None ++ ++ extensions = [] ++ if self.config.ws_per_message_deflate: ++ extensions = [ServerPerMessageDeflateFactory()] ++ self.conn = ServerProtocol( ++ extensions=extensions, ++ max_size=self.config.ws_max_size, ++ logger=logging.getLogger("uvicorn.error"), ++ ) ++ ++ self.read_paused = False ++ self.writable = asyncio.Event() ++ self.writable.set() ++ ++ # Buffers ++ self.bytes = b"" ++ ++ def connection_made(self, transport: BaseTransport) -> None: ++ """Called when a connection is made.""" ++ transport = cast(Transport, transport) ++ self.connections.add(self) ++ self.transport = transport ++ self.server = get_local_addr(transport) ++ self.client = get_remote_addr(transport) ++ self.scheme = "wss" if is_ssl(transport) else "ws" ++ ++ if self.logger.level <= TRACE_LOG_LEVEL: ++ prefix = "%s:%d - " % self.client if self.client else "" ++ self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix) ++ ++ def connection_lost(self, exc: Exception | None) -> None: ++ code = 1005 if self.handshake_complete else 1006 ++ self.queue.put_nowait({"type": "websocket.disconnect", "code": code}) ++ self.connections.remove(self) ++ ++ if self.logger.level <= TRACE_LOG_LEVEL: ++ prefix = "%s:%d - " % self.client if self.client else "" ++ self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix) ++ ++ self.handshake_complete = True ++ if exc is None: ++ self.transport.close() ++ ++ def eof_received(self) -> None: ++ pass ++ ++ def shutdown(self) -> None: ++ if self.handshake_complete: ++ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) ++ self.conn.send_close(1012) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ else: ++ self.send_500_response() ++ self.transport.close() ++ ++ def data_received(self, data: bytes) -> None: ++ self.conn.receive_data(data) ++ parser_exc = self.conn.parser_exc ++ if parser_exc is not None: ++ self.handle_parser_exception() ++ return ++ self.handle_events() ++ ++ def handle_events(self) -> None: ++ for event in self.conn.events_received(): ++ if isinstance(event, Request): ++ self.handle_connect(event) ++ if isinstance(event, Frame): ++ if event.opcode == Opcode.CONT: ++ self.handle_cont(event) ++ elif event.opcode == Opcode.TEXT: ++ self.handle_text(event) ++ elif event.opcode == Opcode.BINARY: ++ self.handle_bytes(event) ++ elif event.opcode == Opcode.PING: ++ self.handle_ping(event) ++ elif event.opcode == Opcode.CLOSE: ++ self.handle_close(event) ++ ++ # Event handlers ++ ++ def handle_connect(self, event: Request) -> None: ++ self.request = event ++ self.response = self.conn.accept(event) ++ self.handshake_initiated = True ++ if self.response.status_code != 101: ++ self.handshake_complete = True ++ self.close_sent = True ++ self.conn.send_response(self.response) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ self.transport.close() ++ return ++ ++ headers = [ ++ (key.encode("ascii"), value.encode("ascii", errors="surrogateescape")) ++ for key, value in event.headers.raw_items() ++ ] ++ raw_path, _, query_string = event.path.partition("?") ++ self.scope: WebSocketScope = { ++ "type": "websocket", ++ "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"}, ++ "http_version": "1.1", ++ "scheme": self.scheme, ++ "server": self.server, ++ "client": self.client, ++ "root_path": self.root_path, ++ "path": unquote(raw_path), ++ "raw_path": raw_path.encode("ascii"), ++ "query_string": query_string.encode("ascii"), ++ "headers": headers, ++ "subprotocols": event.headers.get_all("Sec-WebSocket-Protocol"), ++ "state": self.app_state.copy(), ++ "extensions": {"websocket.http.response": {}}, ++ } ++ self.queue.put_nowait({"type": "websocket.connect"}) ++ task = self.loop.create_task(self.run_asgi()) ++ task.add_done_callback(self.on_task_complete) ++ self.tasks.add(task) ++ ++ def handle_cont(self, event: Frame) -> None: ++ self.bytes += event.data ++ if event.fin: ++ self.send_receive_event_to_app() ++ ++ def handle_text(self, event: Frame) -> None: ++ self.bytes = event.data ++ self.curr_msg_data_type: Literal["text", "bytes"] = "text" ++ if event.fin: ++ self.send_receive_event_to_app() ++ ++ def handle_bytes(self, event: Frame) -> None: ++ self.bytes = event.data ++ self.curr_msg_data_type = "bytes" ++ if event.fin: ++ self.send_receive_event_to_app() ++ ++ def send_receive_event_to_app(self) -> None: ++ data_type = self.curr_msg_data_type ++ msg: WebSocketReceiveEvent ++ if data_type == "text": ++ msg = {"type": "websocket.receive", data_type: self.bytes.decode()} ++ else: ++ msg = {"type": "websocket.receive", data_type: self.bytes} ++ self.queue.put_nowait(msg) ++ if not self.read_paused: ++ self.read_paused = True ++ self.transport.pause_reading() ++ ++ def handle_ping(self, event: Frame) -> None: ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ ++ def handle_close(self, event: Frame) -> None: ++ if not self.close_sent and not self.transport.is_closing(): ++ disconnect_event: WebSocketDisconnectEvent = { ++ "type": "websocket.disconnect", ++ "code": self.conn.close_rcvd.code, # type: ignore[union-attr] ++ "reason": self.conn.close_rcvd.reason, # type: ignore[union-attr] ++ } ++ self.queue.put_nowait(disconnect_event) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ self.transport.close() ++ ++ def handle_parser_exception(self) -> None: ++ disconnect_event: WebSocketDisconnectEvent = { ++ "type": "websocket.disconnect", ++ "code": self.conn.close_sent.code, # type: ignore[union-attr] ++ "reason": self.conn.close_sent.reason, # type: ignore[union-attr] ++ } ++ self.queue.put_nowait(disconnect_event) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ self.close_sent = True ++ self.transport.close() ++ ++ def on_task_complete(self, task: asyncio.Task[None]) -> None: ++ self.tasks.discard(task) ++ ++ async def run_asgi(self) -> None: ++ try: ++ result = await self.app(self.scope, self.receive, self.send) ++ except ClientDisconnected: ++ self.transport.close() ++ except BaseException: ++ self.logger.exception("Exception in ASGI application\n") ++ self.send_500_response() ++ self.transport.close() ++ else: ++ if not self.handshake_complete: ++ msg = "ASGI callable returned without completing handshake." ++ self.logger.error(msg) ++ self.send_500_response() ++ self.transport.close() ++ elif result is not None: ++ msg = "ASGI callable should return None, but returned '%s'." ++ self.logger.error(msg, result) ++ self.transport.close() ++ ++ def send_500_response(self) -> None: ++ if self.initial_response or self.handshake_complete: ++ return ++ response = self.conn.reject(500, "Internal Server Error") ++ self.conn.send_response(response) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ ++ async def send(self, message: ASGISendEvent) -> None: ++ await self.writable.wait() ++ ++ message_type = message["type"] ++ ++ if not self.handshake_complete and self.initial_response is None: ++ if message_type == "websocket.accept": ++ message = cast(WebSocketAcceptEvent, message) ++ self.logger.info( ++ '%s - "WebSocket %s" [accepted]', ++ self.scope["client"], ++ get_path_with_query_string(self.scope), ++ ) ++ headers = [ ++ (name.decode("latin-1").lower(), value.decode("latin-1").lower()) ++ for name, value in (self.default_headers + list(message.get("headers", []))) ++ ] ++ accepted_subprotocol = message.get("subprotocol") ++ if accepted_subprotocol: ++ headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol)) ++ self.response.headers.update(headers) ++ ++ if not self.transport.is_closing(): ++ self.handshake_complete = True ++ self.conn.send_response(self.response) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ ++ elif message_type == "websocket.close": ++ message = cast(WebSocketCloseEvent, message) ++ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) ++ self.logger.info( ++ '%s - "WebSocket %s" 403', ++ self.scope["client"], ++ get_path_with_query_string(self.scope), ++ ) ++ response = self.conn.reject(HTTPStatus.FORBIDDEN, "") ++ self.conn.send_response(response) ++ output = self.conn.data_to_send() ++ self.close_sent = True ++ self.handshake_complete = True ++ self.transport.write(b"".join(output)) ++ self.transport.close() ++ elif message_type == "websocket.http.response.start" and self.initial_response is None: ++ message = cast(WebSocketResponseStartEvent, message) ++ if not (100 <= message["status"] < 600): ++ raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"]) ++ self.logger.info( ++ '%s - "WebSocket %s" %d', ++ self.scope["client"], ++ get_path_with_query_string(self.scope), ++ message["status"], ++ ) ++ headers = [ ++ (name.decode("latin-1"), value.decode("latin-1")) ++ for name, value in list(message.get("headers", [])) ++ ] ++ self.initial_response = (message["status"], headers, b"") ++ else: ++ msg = ( ++ "Expected ASGI message 'websocket.accept', 'websocket.close' " ++ "or 'websocket.http.response.start' " ++ "but got '%s'." ++ ) ++ raise RuntimeError(msg % message_type) ++ ++ elif not self.close_sent and self.initial_response is None: ++ try: ++ if message_type == "websocket.send": ++ message = cast(WebSocketSendEvent, message) ++ bytes_data = message.get("bytes") ++ text_data = message.get("text") ++ if text_data: ++ self.conn.send_text(text_data.encode()) ++ elif bytes_data: ++ self.conn.send_binary(bytes_data) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ ++ elif message_type == "websocket.close" and not self.transport.is_closing(): ++ message = cast(WebSocketCloseEvent, message) ++ code = message.get("code", 1000) ++ reason = message.get("reason", "") or "" ++ self.queue.put_nowait({"type": "websocket.disconnect", "code": code}) ++ self.conn.send_close(code, reason) ++ output = self.conn.data_to_send() ++ self.transport.write(b"".join(output)) ++ self.close_sent = True ++ self.transport.close() ++ else: ++ msg = "Expected ASGI message 'websocket.send' or 'websocket.close'," " but got '%s'." ++ raise RuntimeError(msg % message_type) ++ except InvalidState: ++ raise ClientDisconnected() ++ elif self.initial_response is not None: ++ if message_type == "websocket.http.response.body": ++ message = cast(WebSocketResponseBodyEvent, message) ++ body = self.initial_response[2] + message["body"] ++ self.initial_response = self.initial_response[:2] + (body,) ++ if not message.get("more_body", False): ++ response = self.conn.reject(self.initial_response[0], body.decode()) ++ response.headers.update(self.initial_response[1]) ++ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006}) ++ self.conn.send_response(response) ++ output = self.conn.data_to_send() ++ self.close_sent = True ++ self.transport.write(b"".join(output)) ++ self.transport.close() ++ else: ++ msg = "Expected ASGI message 'websocket.http.response.body' " "but got '%s'." ++ raise RuntimeError(msg % message_type) ++ ++ else: ++ msg = "Unexpected ASGI message '%s', after sending 'websocket.close'." ++ raise RuntimeError(msg % message_type) ++ ++ async def receive(self) -> ASGIReceiveEvent: ++ message = await self.queue.get() ++ if self.read_paused and self.queue.empty(): ++ self.read_paused = False ++ self.transport.resume_reading() ++ return message +diff --git a/uvicorn/server.py b/uvicorn/server.py +index cca2e85..50c5ed2 100644 +--- a/uvicorn/server.py ++++ b/uvicorn/server.py +@@ -23,9 +23,10 @@ if TYPE_CHECKING: + from uvicorn.protocols.http.h11_impl import H11Protocol + from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol + from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol ++ from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol + from uvicorn.protocols.websockets.wsproto_impl import WSProtocol + +- Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol] ++ Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol, WebSocketsSansIOProtocol] + + HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. diff --git a/ilot/uvicorn/2541_bump-wesockets-on-requirements.patch b/ilot/uvicorn/2541_bump-wesockets-on-requirements.patch new file mode 100644 index 0000000..c1179f3 --- /dev/null +++ b/ilot/uvicorn/2541_bump-wesockets-on-requirements.patch @@ -0,0 +1,567 @@ +diff --git a/requirements.txt b/requirements.txt +index e26e6b3..b16569f 100644 +--- a/requirements.txt ++++ b/requirements.txt +@@ -7,7 +7,7 @@ h11 @ git+https://github.com/python-hyper/h11.git@master + # Explicit optionals + a2wsgi==1.10.7 + wsproto==1.2.0 +-websockets==13.1 ++websockets==14.1 + + # Packaging + build==1.2.2.post1 +diff --git a/tests/middleware/test_logging.py b/tests/middleware/test_logging.py +index 63d7daf..5aef174 100644 +--- a/tests/middleware/test_logging.py ++++ b/tests/middleware/test_logging.py +@@ -8,8 +8,7 @@ import typing + + import httpx + import pytest +-import websockets +-import websockets.client ++from websockets.asyncio.client import connect + + from tests.utils import run_server + from uvicorn import Config +@@ -107,8 +106,8 @@ async def test_trace_logging_on_ws_protocol( + break + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.open ++ async with connect(url): ++ return True + + config = Config( + app=websocket_app, +diff --git a/tests/middleware/test_proxy_headers.py b/tests/middleware/test_proxy_headers.py +index d300c45..4b5f195 100644 +--- a/tests/middleware/test_proxy_headers.py ++++ b/tests/middleware/test_proxy_headers.py +@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING + import httpx + import httpx._transports.asgi + import pytest +-import websockets.client ++from websockets.asyncio.client import connect + + from tests.response import Response + from tests.utils import run_server +@@ -479,7 +479,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto( + async with run_server(config): + url = f"ws://127.0.0.1:{unused_tcp_port}" + headers = {X_FORWARDED_FOR: "1.2.3.4", X_FORWARDED_PROTO: forwarded_proto} +- async with websockets.client.connect(url, extra_headers=headers) as websocket: ++ async with connect(url, additional_headers=headers) as websocket: + data = await websocket.recv() + assert data == expected + +diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py +index e728544..b9035ec 100644 +--- a/tests/protocols/test_websocket.py ++++ b/tests/protocols/test_websocket.py +@@ -12,6 +12,8 @@ import websockets.asyncio.client + import websockets.client + import websockets.exceptions + from typing_extensions import TypedDict ++from websockets.asyncio.client import ClientConnection, connect ++from websockets.exceptions import ConnectionClosed, ConnectionClosedError, InvalidHandshake, InvalidStatus + from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory + from websockets.typing import Subprotocol + +@@ -130,8 +132,8 @@ async def test_accept_connection(ws_protocol_cls: WSProtocol, http_protocol_cls: + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.open ++ async with connect(url): ++ return True + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -146,7 +148,7 @@ async def test_shutdown(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProt + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config) as server: +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}"): ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}"): + # Attempt shutdown while connection is still open + await server.shutdown() + +@@ -160,8 +162,8 @@ async def test_supports_permessage_deflate_extension( + + async def open_connection(url: str): + extension_factories = [ClientPerMessageDeflateFactory()] +- async with websockets.client.connect(url, extensions=extension_factories) as websocket: +- return [extension.name for extension in websocket.extensions] ++ async with connect(url, extensions=extension_factories) as websocket: ++ return [extension.name for extension in websocket.protocol.extensions] + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -180,8 +182,8 @@ async def test_can_disable_permessage_deflate_extension( + # enable per-message deflate on the client, so that we can check the server + # won't support it when it's disabled. + extension_factories = [ClientPerMessageDeflateFactory()] +- async with websockets.client.connect(url, extensions=extension_factories) as websocket: +- return [extension.name for extension in websocket.extensions] ++ async with connect(url, extensions=extension_factories) as websocket: ++ return [extension.name for extension in websocket.protocol.extensions] + + config = Config( + app=App, +@@ -203,8 +205,8 @@ async def test_close_connection(ws_protocol_cls: WSProtocol, http_protocol_cls: + + async def open_connection(url: str): + try: +- await websockets.client.connect(url) +- except websockets.exceptions.InvalidHandshake: ++ await connect(url) ++ except InvalidHandshake: + return False + return True # pragma: no cover + +@@ -224,8 +226,8 @@ async def test_headers(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProto + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url, extra_headers=[("username", "abraão")]) as websocket: +- return websocket.open ++ async with connect(url, additional_headers=[("username", "abraão")]): ++ return True + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -239,8 +241,9 @@ async def test_extra_headers(ws_protocol_cls: WSProtocol, http_protocol_cls: HTT + await self.send({"type": "websocket.accept", "headers": [(b"extra", b"header")]}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.response_headers ++ async with connect(url) as websocket: ++ assert websocket.response ++ return websocket.response.headers + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -258,8 +261,8 @@ async def test_path_and_raw_path(ws_protocol_cls: WSProtocol, http_protocol_cls: + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.open ++ async with connect(url): ++ return True + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -276,7 +279,7 @@ async def test_send_text_data_to_client( + await self.send({"type": "websocket.send", "text": "123"}) + + async def get_data(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + return await websocket.recv() + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) +@@ -294,7 +297,7 @@ async def test_send_binary_data_to_client( + await self.send({"type": "websocket.send", "bytes": b"123"}) + + async def get_data(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + return await websocket.recv() + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) +@@ -313,7 +316,7 @@ async def test_send_and_close_connection( + await self.send({"type": "websocket.close"}) + + async def get_data(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + data = await websocket.recv() + is_open = True + try: +@@ -342,7 +345,7 @@ async def test_send_text_data_to_server( + await self.send({"type": "websocket.send", "text": _text}) + + async def send_text(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + await websocket.send("abc") + return await websocket.recv() + +@@ -365,7 +368,7 @@ async def test_send_binary_data_to_server( + await self.send({"type": "websocket.send", "bytes": _bytes}) + + async def send_text(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + await websocket.send(b"abc") + return await websocket.recv() + +@@ -387,7 +390,7 @@ async def test_send_after_protocol_close( + await self.send({"type": "websocket.send", "text": "123"}) + + async def get_data(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + data = await websocket.recv() + is_open = True + try: +@@ -407,14 +410,14 @@ async def test_missing_handshake(ws_protocol_cls: WSProtocol, http_protocol_cls: + async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): + pass + +- async def connect(url: str): +- await websockets.client.connect(url) ++ async def open_connection(url: str): ++ await connect(url) + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: +- await connect(f"ws://127.0.0.1:{unused_tcp_port}") +- assert exc_info.value.status_code == 500 ++ with pytest.raises(InvalidStatus) as exc_info: ++ await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") ++ assert exc_info.value.response.status_code == 500 + + + async def test_send_before_handshake( +@@ -423,14 +426,14 @@ async def test_send_before_handshake( + async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): + await send({"type": "websocket.send", "text": "123"}) + +- async def connect(url: str): +- await websockets.client.connect(url) ++ async def open_connection(url: str): ++ await connect(url) + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: +- await connect(f"ws://127.0.0.1:{unused_tcp_port}") +- assert exc_info.value.status_code == 500 ++ with pytest.raises(InvalidStatus) as exc_info: ++ await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") ++ assert exc_info.value.response.status_code == 500 + + + async def test_duplicate_handshake(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): +@@ -440,10 +443,10 @@ async def test_duplicate_handshake(ws_protocol_cls: WSProtocol, http_protocol_cl + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: +- with pytest.raises(websockets.exceptions.ConnectionClosed): ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: ++ with pytest.raises(ConnectionClosed): + _ = await websocket.recv() +- assert websocket.close_code == 1006 ++ assert websocket.protocol.close_code == 1006 + + + async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): +@@ -458,10 +461,10 @@ async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls: + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: +- with pytest.raises(websockets.exceptions.ConnectionClosed): ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: ++ with pytest.raises(ConnectionClosed): + _ = await websocket.recv() +- assert websocket.close_code == 1006 ++ assert websocket.protocol.close_code == 1006 + + + @pytest.mark.parametrize("code", [None, 1000, 1001]) +@@ -493,13 +496,13 @@ async def test_app_close( + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + await websocket.ping() + await websocket.send("abc") +- with pytest.raises(websockets.exceptions.ConnectionClosed): ++ with pytest.raises(ConnectionClosed): + await websocket.recv() +- assert websocket.close_code == (code or 1000) +- assert websocket.close_reason == (reason or "") ++ assert websocket.protocol.close_code == (code or 1000) ++ assert websocket.protocol.close_reason == (reason or "") + + + async def test_client_close(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int): +@@ -518,7 +521,7 @@ async def test_client_close(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTP + break + + async def websocket_session(url: str): +- async with websockets.client.connect(url) as websocket: ++ async with connect(url) as websocket: + await websocket.ping() + await websocket.send("abc") + await websocket.close(code=1001, reason="custom reason") +@@ -555,7 +558,7 @@ async def test_client_connection_lost( + port=unused_tcp_port, + ) + async with run_server(config): +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + websocket.transport.close() + await asyncio.sleep(0.1) + got_disconnect_event_before_shutdown = got_disconnect_event +@@ -583,7 +586,7 @@ async def test_client_connection_lost_on_send( + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): + url = f"ws://127.0.0.1:{unused_tcp_port}" +- async with websockets.client.connect(url): ++ async with connect(url): + await asyncio.sleep(0.1) + disconnect.set() + +@@ -642,11 +645,11 @@ async def test_send_close_on_server_shutdown( + disconnect_message = message + break + +- websocket: websockets.client.WebSocketClientProtocol | None = None ++ websocket: ClientConnection | None = None + + async def websocket_session(uri: str): + nonlocal websocket +- async with websockets.client.connect(uri) as ws_connection: ++ async with connect(uri) as ws_connection: + websocket = ws_connection + await server_shutdown_event.wait() + +@@ -676,9 +679,7 @@ async def test_subprotocols( + await self.send({"type": "websocket.accept", "subprotocol": subprotocol}) + + async def get_subprotocol(url: str): +- async with websockets.client.connect( +- url, subprotocols=[Subprotocol("proto1"), Subprotocol("proto2")] +- ) as websocket: ++ async with connect(url, subprotocols=[Subprotocol("proto1"), Subprotocol("proto2")]) as websocket: + return websocket.subprotocol + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) +@@ -688,7 +689,7 @@ async def test_subprotocols( + + + MAX_WS_BYTES = 1024 * 1024 * 16 +-MAX_WS_BYTES_PLUS1 = MAX_WS_BYTES + 1 ++MAX_WS_BYTES_PLUS1 = MAX_WS_BYTES + 10 + + + @pytest.mark.parametrize( +@@ -731,15 +732,15 @@ async def test_send_binary_data_to_server_bigger_than_default_on_websockets( + port=unused_tcp_port, + ) + async with run_server(config): +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}", max_size=client_size_sent) as ws: ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}", max_size=client_size_sent) as ws: + await ws.send(b"\x01" * client_size_sent) + if expected_result == 0: + data = await ws.recv() + assert data == b"\x01" * client_size_sent + else: +- with pytest.raises(websockets.exceptions.ConnectionClosedError): ++ with pytest.raises(ConnectionClosedError): + await ws.recv() +- assert ws.close_code == expected_result ++ assert ws.protocol.close_code == expected_result + + + async def test_server_reject_connection( +@@ -764,10 +765,10 @@ async def test_server_reject_connection( + disconnected_message = await receive() + + async def websocket_session(url: str): +- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: +- async with websockets.client.connect(url): ++ with pytest.raises(InvalidStatus) as exc_info: ++ async with connect(url): + pass # pragma: no cover +- assert exc_info.value.status_code == 403 ++ assert exc_info.value.response.status_code == 403 + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -937,10 +938,10 @@ async def test_server_reject_connection_with_invalid_msg( + await send(message) + + async def websocket_session(url: str): +- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: +- async with websockets.client.connect(url): ++ with pytest.raises(InvalidStatus) as exc_info: ++ async with connect(url): + pass # pragma: no cover +- assert exc_info.value.status_code == 404 ++ assert exc_info.value.response.status_code == 404 + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -971,10 +972,10 @@ async def test_server_reject_connection_with_missing_body( + # no further message + + async def websocket_session(url: str): +- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: +- async with websockets.client.connect(url): ++ with pytest.raises(InvalidStatus) as exc_info: ++ async with connect(url): + pass # pragma: no cover +- assert exc_info.value.status_code == 404 ++ assert exc_info.value.response.status_code == 404 + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -1014,17 +1015,17 @@ async def test_server_multiple_websocket_http_response_start_events( + exception_message = str(exc) + + async def websocket_session(url: str): +- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info: +- async with websockets.client.connect(url): ++ with pytest.raises(InvalidStatus) as exc_info: ++ async with connect(url): + pass # pragma: no cover +- assert exc_info.value.status_code == 404 ++ assert exc_info.value.response.status_code == 404 + + config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): + await websocket_session(f"ws://127.0.0.1:{unused_tcp_port}") + + assert exception_message == ( +- "Expected ASGI message 'websocket.http.response.body' but got " "'websocket.http.response.start'." ++ "Expected ASGI message 'websocket.http.response.body' but got 'websocket.http.response.start'." + ) + + +@@ -1053,7 +1054,7 @@ async def test_server_can_read_messages_in_buffer_after_close( + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: ++ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket: + await websocket.send(b"abc") + await websocket.send(b"abc") + await websocket.send(b"abc") +@@ -1070,8 +1071,9 @@ async def test_default_server_headers( + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.response_headers ++ async with connect(url) as websocket: ++ assert websocket.response ++ return websocket.response.headers + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -1085,8 +1087,9 @@ async def test_no_server_headers(ws_protocol_cls: WSProtocol, http_protocol_cls: + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.response_headers ++ async with connect(url) as websocket: ++ assert websocket.response ++ return websocket.response.headers + + config = Config( + app=App, +@@ -1108,8 +1111,9 @@ async def test_no_date_header_on_wsproto(http_protocol_cls: HTTPProtocol, unused + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.response_headers ++ async with connect(url) as websocket: ++ assert websocket.response ++ return websocket.response.headers + + config = Config( + app=App, +@@ -1140,8 +1144,9 @@ async def test_multiple_server_header( + ) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.response_headers ++ async with connect(url) as websocket: ++ assert websocket.response ++ return websocket.response.headers + + config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port) + async with run_server(config): +@@ -1176,8 +1181,8 @@ async def test_lifespan_state(ws_protocol_cls: WSProtocol, http_protocol_cls: HT + await self.send({"type": "websocket.accept"}) + + async def open_connection(url: str): +- async with websockets.client.connect(url) as websocket: +- return websocket.open ++ async with connect(url): ++ return True + + async def app_wrapper(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): + if scope["type"] == "lifespan": +diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py +index cd6c54f..685d6b6 100644 +--- a/uvicorn/protocols/websockets/websockets_impl.py ++++ b/uvicorn/protocols/websockets/websockets_impl.py +@@ -13,8 +13,7 @@ from websockets.datastructures import Headers + from websockets.exceptions import ConnectionClosed + from websockets.extensions.base import ServerExtensionFactory + from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory +-from websockets.legacy.server import HTTPResponse +-from websockets.server import WebSocketServerProtocol ++from websockets.legacy.server import HTTPResponse, WebSocketServerProtocol + from websockets.typing import Subprotocol + + from uvicorn._types import ( +diff --git a/uvicorn/protocols/websockets/wsproto_impl.py b/uvicorn/protocols/websockets/wsproto_impl.py +index 828afe5..5d84bff 100644 +--- a/uvicorn/protocols/websockets/wsproto_impl.py ++++ b/uvicorn/protocols/websockets/wsproto_impl.py +@@ -149,12 +149,13 @@ class WSProtocol(asyncio.Protocol): + self.writable.set() # pragma: full coverage + + def shutdown(self) -> None: +- if self.handshake_complete: +- self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) +- output = self.conn.send(wsproto.events.CloseConnection(code=1012)) +- self.transport.write(output) +- else: +- self.send_500_response() ++ if not self.response_started: ++ if self.handshake_complete: ++ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012}) ++ output = self.conn.send(wsproto.events.CloseConnection(code=1012)) ++ self.transport.write(output) ++ else: ++ self.send_500_response() + self.transport.close() + + def on_task_complete(self, task: asyncio.Task[None]) -> None: +@@ -221,13 +222,15 @@ class WSProtocol(asyncio.Protocol): + def send_500_response(self) -> None: + if self.response_started or self.handshake_complete: + return # we cannot send responses anymore ++ reject_data = b"Internal Server Error" + headers: list[tuple[bytes, bytes]] = [ + (b"content-type", b"text/plain; charset=utf-8"), ++ (b"content-length", str(len(reject_data)).encode()), + (b"connection", b"close"), + (b"content-length", b"21"), + ] + output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True)) +- output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error")) ++ output += self.conn.send(wsproto.events.RejectData(data=reject_data)) + self.transport.write(output) + + async def run_asgi(self) -> None: diff --git a/ilot/uvicorn/APKBUILD b/ilot/uvicorn/APKBUILD new file mode 100644 index 0000000..1f14918 --- /dev/null +++ b/ilot/uvicorn/APKBUILD @@ -0,0 +1,59 @@ +maintainer="Michał Polański " +pkgname=uvicorn +pkgver=0.34.0 +pkgrel=0 +pkgdesc="Lightning-fast ASGI server" +url="https://www.uvicorn.org/" +license="BSD-3-Clause" +# disable due to lack of support for websockets 14 +# https://gitlab.alpinelinux.org/alpine/aports/-/issues/16646 +arch="noarch" +depends="py3-click py3-h11" +makedepends="py3-gpep517 py3-hatchling" +checkdepends=" + py3-a2wsgi + py3-dotenv + py3-httptools + py3-httpx + py3-pytest + py3-pytest-mock + py3-trustme + py3-typing-extensions + py3-watchfiles + py3-websockets + py3-wsproto + py3-yaml + " +subpackages="$pkgname-pyc" +source="https://github.com/encode/uvicorn/archive/$pkgver/uvicorn-$pkgver.tar.gz + test_multiprocess.patch + 2540_add-websocketssansioprotocol.patch + 2541_bump-wesockets-on-requirements.patch + fix-test-wsgi.patch + " + +build() { + gpep517 build-wheel \ + --wheel-dir .dist \ + --output-fd 3 3>&1 >&2 +} + +check() { + python3 -m venv --clear --without-pip --system-site-packages .testenv + .testenv/bin/python3 -m installer .dist/*.whl + .testenv/bin/python3 -m pytest \ + -k "not test_close_connection_with_multiple_requests" # a known issue +} + +package() { + python3 -m installer -d "$pkgdir" \ + .dist/uvicorn-$pkgver-py3-none-any.whl +} + +sha512sums=" +260782e385a2934049da8c474750958826afe1bfe23b38fe2f6420f355af7a537563f8fe6ac3830814c7469203703d10f4f9f3d6e53e79113bfd2fd34f7a7c72 uvicorn-0.34.0.tar.gz +cfad91dd84f8974362f52d754d7a29f09d07927a46acaa0eb490b6115a5729d84d6df94fead10ccd4cce7f5ea376f1348b0f59daede661dd8373a3851c313c46 test_multiprocess.patch +858e9a7baaf1c12e076aecd81aaaf622b35a59dcaabea4ee1bfc4cda704c9fe271b1cc616a5910d845393717e4989cecb3b04be249cb5d0df1001ec5224c293f 2540_add-websocketssansioprotocol.patch +f8a8c190981b9070232ea985880685bc801947cc7f673d59abf73d3e68bc2e13515ad200232a1de2af0808bc85da48a341f57d47caf87bcc190bfdc3c45718e0 2541_bump-wesockets-on-requirements.patch +379963f9ccbda013e4a0bc3441eee70a581c91f60206aedc15df6a8737950824b7cb8d867774fc415763449bb3e0bba66601e8551101bfc1741098acd035f0cc fix-test-wsgi.patch +" diff --git a/ilot/uvicorn/fix-test-wsgi.patch b/ilot/uvicorn/fix-test-wsgi.patch new file mode 100644 index 0000000..ed49e52 --- /dev/null +++ b/ilot/uvicorn/fix-test-wsgi.patch @@ -0,0 +1,13 @@ +diff --git a/tests/middleware/test_wsgi.py.orig b/tests/middleware/test_wsgi.py +index 6003f27..2750487 100644 +--- a/tests/middleware/test_wsgi.py.orig ++++ b/tests/middleware/test_wsgi.py +@@ -73,7 +73,7 @@ async def test_wsgi_post(wsgi_middleware: Callable) -> None: + async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: + response = await client.post("/", json={"example": 123}) + assert response.status_code == 200 +- assert response.text == '{"example":123}' ++ assert response.text == '{"example": 123}' + + + @pytest.mark.anyio diff --git a/ilot/uvicorn/test_multiprocess.patch b/ilot/uvicorn/test_multiprocess.patch new file mode 100644 index 0000000..231526e --- /dev/null +++ b/ilot/uvicorn/test_multiprocess.patch @@ -0,0 +1,14 @@ +Wait a bit longer, otherwise the workers might +not have time to finish restarting. + +--- a/tests/supervisors/test_multiprocess.py ++++ b/tests/supervisors/test_multiprocess.py +@@ -132,7 +132,7 @@ def test_multiprocess_sighup() -> None: + time.sleep(1) + pids = [p.pid for p in supervisor.processes] + supervisor.signal_queue.append(signal.SIGHUP) +- time.sleep(1) ++ time.sleep(3) + assert pids != [p.pid for p in supervisor.processes] + supervisor.signal_queue.append(signal.SIGINT) + supervisor.join_all()