backports/uvicorn: new aport
Some checks failed
/ deploy-aarch64 (pull_request) Has been cancelled
/ build-aarch64 (pull_request) Has been cancelled
/ deploy-x86_64 (pull_request) Has been cancelled
/ build-x86_64 (pull_request) Has been cancelled
/ lint (pull_request) Failing after 27s

This commit is contained in:
Antoine Martin 2025-01-05 11:22:58 -05:00
parent afaf7e2a4e
commit 16d4d704b0
Signed by: forge
GPG key ID: D62A472A4AA7D541
4 changed files with 1256 additions and 0 deletions

View file

@ -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.

View file

@ -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:

View file

@ -0,0 +1,57 @@
maintainer="Michał Polański <michal@polanski.me>"
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
"
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
"

View file

@ -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()