uvicorn 0.34.2__tar.gz → 0.35.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {uvicorn-0.34.2 → uvicorn-0.35.0}/PKG-INFO +2 -2
- {uvicorn-0.34.2 → uvicorn-0.35.0}/pyproject.toml +4 -1
- {uvicorn-0.34.2 → uvicorn-0.35.0}/requirements.txt +5 -6
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/conftest.py +2 -2
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_logging.py +14 -11
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_proxy_headers.py +1 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/test_websocket.py +14 -12
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/test_reload.py +6 -14
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_config.py +6 -6
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/__init__.py +1 -1
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/config.py +3 -2
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/main.py +1 -1
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/auto.py +2 -2
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/websockets_impl.py +4 -3
- uvicorn-0.35.0/uvicorn/protocols/websockets/websockets_sansio_impl.py +417 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/wsproto_impl.py +11 -11
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/server.py +2 -1
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/watchfilesreload.py +1 -4
- {uvicorn-0.34.2 → uvicorn-0.35.0}/.gitignore +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/LICENSE.md +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/README.md +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/circular_import_a.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/circular_import_b.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/raise_import_error.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/importer/test_importer.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_message_logger.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/middleware/test_wsgi.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/test_http.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/protocols/test_utils.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/response.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/test_multiprocess.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/supervisors/test_signal.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_auto_detection.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_cli.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_default_headers.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_lifespan.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_main.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_server.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_ssl.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/test_subprocess.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/tests/utils.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/__main__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/_subprocess.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/_types.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/importer.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/lifespan/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/lifespan/off.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/lifespan/on.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/logging.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/asyncio.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/auto.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/loops/uvloop.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/asgi2.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/message_logger.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/proxy_headers.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/middleware/wsgi.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/auto.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/flow_control.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/h11_impl.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/http/httptools_impl.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/utils.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/protocols/websockets/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/py.typed +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/__init__.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/basereload.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/multiprocess.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/supervisors/statreload.py +0 -0
- {uvicorn-0.34.2 → uvicorn-0.35.0}/uvicorn/workers.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: uvicorn
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.35.0
|
4
4
|
Summary: The lightning-fast ASGI server.
|
5
5
|
Project-URL: Changelog, https://www.uvicorn.org/release-notes
|
6
6
|
Project-URL: Funding, https://github.com/sponsors/encode
|
@@ -32,7 +32,7 @@ Requires-Dist: colorama>=0.4; (sys_platform == 'win32') and extra == 'standard'
|
|
32
32
|
Requires-Dist: httptools>=0.6.3; extra == 'standard'
|
33
33
|
Requires-Dist: python-dotenv>=0.13; extra == 'standard'
|
34
34
|
Requires-Dist: pyyaml>=5.1; extra == 'standard'
|
35
|
-
Requires-Dist: uvloop
|
35
|
+
Requires-Dist: uvloop>=0.15.1; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard'
|
36
36
|
Requires-Dist: watchfiles>=0.13; extra == 'standard'
|
37
37
|
Requires-Dist: websockets>=10.4; extra == 'standard'
|
38
38
|
Description-Content-Type: text/markdown
|
@@ -41,7 +41,7 @@ standard = [
|
|
41
41
|
"httptools>=0.6.3",
|
42
42
|
"python-dotenv>=0.13",
|
43
43
|
"PyYAML>=5.1",
|
44
|
-
"uvloop>=0.
|
44
|
+
"uvloop>=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
|
45
45
|
"watchfiles>=0.13",
|
46
46
|
"websockets>=10.4",
|
47
47
|
]
|
@@ -92,6 +92,9 @@ filterwarnings = [
|
|
92
92
|
"ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
|
93
93
|
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
|
94
94
|
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
|
95
|
+
"ignore: websockets.legacy is deprecated.*:DeprecationWarning",
|
96
|
+
"ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning",
|
97
|
+
"ignore: websockets.client.connect is deprecated.*:DeprecationWarning",
|
95
98
|
]
|
96
99
|
|
97
100
|
[tool.coverage.run]
|
@@ -1,8 +1,7 @@
|
|
1
1
|
-e .[standard]
|
2
2
|
|
3
|
-
# TODO: Remove this after h11 makes a release. By this writing, h11 was on 0.14.0.
|
4
3
|
# Core dependencies
|
5
|
-
h11
|
4
|
+
h11==0.16.0
|
6
5
|
|
7
6
|
# Explicit optionals
|
8
7
|
a2wsgi==1.10.8
|
@@ -14,15 +13,15 @@ build==1.2.2.post1
|
|
14
13
|
twine==6.1.0
|
15
14
|
|
16
15
|
# Testing
|
17
|
-
ruff==0.11.
|
16
|
+
ruff==0.11.9
|
18
17
|
pytest==8.3.5
|
19
18
|
pytest-mock==3.14.0
|
20
19
|
pytest-xdist[psutil]==3.6.1
|
21
20
|
mypy==1.15.0
|
22
21
|
types-click==7.1.8
|
23
|
-
types-pyyaml==6.0.12.
|
22
|
+
types-pyyaml==6.0.12.20250402
|
24
23
|
trustme==1.2.1
|
25
|
-
cryptography==44.0.
|
24
|
+
cryptography==44.0.3
|
26
25
|
coverage==7.8.0
|
27
26
|
coverage-conditional-plugin==0.9.0
|
28
27
|
coverage-enable-subprocess==1.0
|
@@ -30,4 +29,4 @@ httpx==0.28.1
|
|
30
29
|
|
31
30
|
# Documentation
|
32
31
|
mkdocs==1.6.1
|
33
|
-
mkdocs-material==9.6.
|
32
|
+
mkdocs-material==9.6.13
|
@@ -233,9 +233,9 @@ def unused_tcp_port() -> int:
|
|
233
233
|
marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
|
234
234
|
id="wsproto",
|
235
235
|
),
|
236
|
+
pytest.param("uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", id="websockets"),
|
236
237
|
pytest.param(
|
237
|
-
"uvicorn.protocols.websockets.
|
238
|
-
id="websockets",
|
238
|
+
"uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio"
|
239
239
|
),
|
240
240
|
]
|
241
241
|
)
|
@@ -4,18 +4,19 @@ import contextlib
|
|
4
4
|
import logging
|
5
5
|
import socket
|
6
6
|
import sys
|
7
|
-
import
|
7
|
+
from collections.abc import Iterator
|
8
|
+
from typing import TYPE_CHECKING, Any
|
8
9
|
|
9
10
|
import httpx
|
10
11
|
import pytest
|
11
|
-
import websockets
|
12
12
|
import websockets.client
|
13
|
+
from websockets.protocol import State
|
13
14
|
|
14
15
|
from tests.utils import run_server
|
15
16
|
from uvicorn import Config
|
16
17
|
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
17
18
|
|
18
|
-
if
|
19
|
+
if TYPE_CHECKING:
|
19
20
|
import sys
|
20
21
|
|
21
22
|
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
@@ -32,7 +33,7 @@ pytestmark = pytest.mark.anyio
|
|
32
33
|
|
33
34
|
|
34
35
|
@contextlib.contextmanager
|
35
|
-
def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) ->
|
36
|
+
def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> Iterator[pytest.LogCaptureFixture]:
|
36
37
|
logger = logging.getLogger(logger_name)
|
37
38
|
logger.propagate, old_propagate = False, logger.propagate
|
38
39
|
logger.addHandler(caplog.handler)
|
@@ -49,7 +50,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
|
|
49
50
|
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
50
51
|
|
51
52
|
|
52
|
-
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
|
53
|
+
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int):
|
53
54
|
config = Config(
|
54
55
|
app=app,
|
55
56
|
log_level="trace",
|
@@ -91,8 +92,8 @@ async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging
|
|
91
92
|
|
92
93
|
async def test_trace_logging_on_ws_protocol(
|
93
94
|
ws_protocol_cls: WSProtocol,
|
94
|
-
caplog,
|
95
|
-
logging_config,
|
95
|
+
caplog: pytest.LogCaptureFixture,
|
96
|
+
logging_config: dict[str, Any],
|
96
97
|
unused_tcp_port: int,
|
97
98
|
):
|
98
99
|
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
@@ -104,9 +105,9 @@ async def test_trace_logging_on_ws_protocol(
|
|
104
105
|
elif message["type"] == "websocket.disconnect":
|
105
106
|
break
|
106
107
|
|
107
|
-
async def open_connection(url):
|
108
|
+
async def open_connection(url: str):
|
108
109
|
async with websockets.client.connect(url) as websocket:
|
109
|
-
return websocket.
|
110
|
+
return websocket.state is State.OPEN
|
110
111
|
|
111
112
|
config = Config(
|
112
113
|
app=websocket_app,
|
@@ -126,7 +127,9 @@ async def test_trace_logging_on_ws_protocol(
|
|
126
127
|
|
127
128
|
|
128
129
|
@pytest.mark.parametrize("use_colors", [(True), (False), (None)])
|
129
|
-
async def test_access_logging(
|
130
|
+
async def test_access_logging(
|
131
|
+
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
|
132
|
+
):
|
130
133
|
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
|
131
134
|
with caplog_for_logger(caplog, "uvicorn.access"):
|
132
135
|
async with run_server(config):
|
@@ -140,7 +143,7 @@ async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture
|
|
140
143
|
|
141
144
|
@pytest.mark.parametrize("use_colors", [(True), (False)])
|
142
145
|
async def test_default_logging(
|
143
|
-
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int
|
146
|
+
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
|
144
147
|
):
|
145
148
|
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
|
146
149
|
with caplog_for_logger(caplog, "uvicorn.access"):
|
@@ -465,6 +465,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
|
|
465
465
|
host, port = scope["client"]
|
466
466
|
await send({"type": "websocket.accept"})
|
467
467
|
await send({"type": "websocket.send", "text": f"{scheme}://{host}:{port}"})
|
468
|
+
await send({"type": "websocket.close"})
|
468
469
|
|
469
470
|
app_with_middleware = ProxyHeadersMiddleware(websocket_app, trusted_hosts="*")
|
470
471
|
config = Config(
|
@@ -1,8 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import typing
|
5
4
|
from copy import deepcopy
|
5
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
6
6
|
|
7
7
|
import httpx
|
8
8
|
import pytest
|
@@ -35,7 +35,7 @@ try:
|
|
35
35
|
except ModuleNotFoundError: # pragma: no cover
|
36
36
|
skip_if_no_wsproto = pytest.mark.skipif(True, reason="wsproto is not installed.")
|
37
37
|
|
38
|
-
if
|
38
|
+
if TYPE_CHECKING:
|
39
39
|
import sys
|
40
40
|
|
41
41
|
from uvicorn.protocols.http.h11_impl import H11Protocol
|
@@ -597,12 +597,9 @@ async def test_connection_lost_before_handshake_complete(
|
|
597
597
|
await send_accept_task.wait()
|
598
598
|
disconnect_message = await receive() # type: ignore
|
599
599
|
|
600
|
-
response: httpx.Response | None = None
|
601
|
-
|
602
600
|
async def websocket_session(uri: str):
|
603
|
-
nonlocal response
|
604
601
|
async with httpx.AsyncClient() as client:
|
605
|
-
|
602
|
+
await client.get(
|
606
603
|
f"http://127.0.0.1:{unused_tcp_port}",
|
607
604
|
headers={
|
608
605
|
"upgrade": "websocket",
|
@@ -619,9 +616,6 @@ async def test_connection_lost_before_handshake_complete(
|
|
619
616
|
send_accept_task.set()
|
620
617
|
await asyncio.sleep(0.1)
|
621
618
|
|
622
|
-
assert response is not None
|
623
|
-
assert response.status_code == 500, response.text
|
624
|
-
assert response.text == "Internal Server Error"
|
625
619
|
assert disconnect_message == {"type": "websocket.disconnect", "code": 1006}
|
626
620
|
await task
|
627
621
|
|
@@ -776,7 +770,7 @@ async def test_server_reject_connection(
|
|
776
770
|
assert disconnected_message == {"type": "websocket.disconnect", "code": 1006}
|
777
771
|
|
778
772
|
|
779
|
-
class EmptyDict(
|
773
|
+
class EmptyDict(TypedDict): ...
|
780
774
|
|
781
775
|
|
782
776
|
async def test_server_reject_connection_with_response(
|
@@ -916,6 +910,9 @@ async def test_server_reject_connection_with_body_nolength(
|
|
916
910
|
async def test_server_reject_connection_with_invalid_msg(
|
917
911
|
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
|
918
912
|
):
|
913
|
+
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
|
914
|
+
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
|
915
|
+
|
919
916
|
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
920
917
|
assert scope["type"] == "websocket"
|
921
918
|
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
|
@@ -947,6 +944,9 @@ async def test_server_reject_connection_with_invalid_msg(
|
|
947
944
|
async def test_server_reject_connection_with_missing_body(
|
948
945
|
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
|
949
946
|
):
|
947
|
+
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
|
948
|
+
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
|
949
|
+
|
950
950
|
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
951
951
|
assert scope["type"] == "websocket"
|
952
952
|
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
|
@@ -982,6 +982,8 @@ async def test_server_multiple_websocket_http_response_start_events(
|
|
982
982
|
The server should raise an exception if it sends multiple
|
983
983
|
websocket.http.response.start events.
|
984
984
|
"""
|
985
|
+
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
|
986
|
+
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
|
985
987
|
exception_message: str | None = None
|
986
988
|
|
987
989
|
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
@@ -1142,12 +1144,12 @@ async def test_multiple_server_header(
|
|
1142
1144
|
|
1143
1145
|
|
1144
1146
|
async def test_lifespan_state(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
|
1145
|
-
expected_states: list[dict[str,
|
1147
|
+
expected_states: list[dict[str, Any]] = [
|
1146
1148
|
{"a": 123, "b": [1]},
|
1147
1149
|
{"a": 123, "b": [1, 2]},
|
1148
1150
|
]
|
1149
1151
|
|
1150
|
-
actual_states: list[dict[str,
|
1152
|
+
actual_states: list[dict[str, Any]] = []
|
1151
1153
|
|
1152
1154
|
async def lifespan_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
1153
1155
|
message = await receive()
|
@@ -306,24 +306,17 @@ class TestBaseReload:
|
|
306
306
|
|
307
307
|
|
308
308
|
@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
|
309
|
-
def
|
309
|
+
def test_should_watch_cwd(mocker: MockerFixture, reload_directory_structure: Path):
|
310
310
|
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
|
311
|
-
app_dir = reload_directory_structure / "app"
|
312
|
-
app_first_dir = reload_directory_structure / "app_first"
|
313
311
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
reload_dirs=[str(app_dir), str(app_first_dir)],
|
319
|
-
)
|
320
|
-
WatchFilesReload(config, target=run, sockets=[])
|
321
|
-
mock_watch.assert_called_once()
|
322
|
-
assert mock_watch.call_args[0] == (Path.cwd(),)
|
312
|
+
config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=[])
|
313
|
+
WatchFilesReload(config, target=run, sockets=[])
|
314
|
+
mock_watch.assert_called_once()
|
315
|
+
assert mock_watch.call_args[0] == (Path.cwd(),)
|
323
316
|
|
324
317
|
|
325
318
|
@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
|
326
|
-
def
|
319
|
+
def test_should_watch_multiple_dirs(mocker: MockerFixture, reload_directory_structure: Path):
|
327
320
|
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
|
328
321
|
app_dir = reload_directory_structure / "app"
|
329
322
|
app_first_dir = reload_directory_structure / "app_first"
|
@@ -337,7 +330,6 @@ def test_should_watch_separate_dirs_outside_cwd(mocker: MockerFixture, reload_di
|
|
337
330
|
assert set(mock_watch.call_args[0]) == {
|
338
331
|
app_dir,
|
339
332
|
app_first_dir,
|
340
|
-
Path.cwd(),
|
341
333
|
}
|
342
334
|
|
343
335
|
|
@@ -7,9 +7,9 @@ import logging
|
|
7
7
|
import os
|
8
8
|
import socket
|
9
9
|
import sys
|
10
|
-
import
|
10
|
+
from collections.abc import Iterator
|
11
11
|
from pathlib import Path
|
12
|
-
from typing import Any, Literal
|
12
|
+
from typing import IO, Any, Callable, Literal
|
13
13
|
from unittest.mock import MagicMock
|
14
14
|
|
15
15
|
import pytest
|
@@ -291,7 +291,7 @@ def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None:
|
|
291
291
|
assert config.is_ssl is True
|
292
292
|
|
293
293
|
|
294
|
-
def asgi2_app(scope: Scope) ->
|
294
|
+
def asgi2_app(scope: Scope) -> Callable:
|
295
295
|
async def asgi(receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: # pragma: nocover
|
296
296
|
pass
|
297
297
|
|
@@ -374,7 +374,7 @@ def test_log_config_yaml(
|
|
374
374
|
@pytest.mark.parametrize("config_file", ["log_config.ini", configparser.ConfigParser(), io.StringIO()])
|
375
375
|
def test_log_config_file(
|
376
376
|
mocked_logging_config_module: MagicMock,
|
377
|
-
config_file: str | configparser.RawConfigParser |
|
377
|
+
config_file: str | configparser.RawConfigParser | IO[Any],
|
378
378
|
) -> None:
|
379
379
|
"""
|
380
380
|
Test that one can load a configparser config from disk.
|
@@ -386,14 +386,14 @@ def test_log_config_file(
|
|
386
386
|
|
387
387
|
|
388
388
|
@pytest.fixture(params=[0, 1])
|
389
|
-
def web_concurrency(request: pytest.FixtureRequest) ->
|
389
|
+
def web_concurrency(request: pytest.FixtureRequest) -> Iterator[int]:
|
390
390
|
yield request.param
|
391
391
|
if os.getenv("WEB_CONCURRENCY"):
|
392
392
|
del os.environ["WEB_CONCURRENCY"]
|
393
393
|
|
394
394
|
|
395
395
|
@pytest.fixture(params=["127.0.0.1", "127.0.0.2"])
|
396
|
-
def forwarded_allow_ips(request: pytest.FixtureRequest) ->
|
396
|
+
def forwarded_allow_ips(request: pytest.FixtureRequest) -> Iterator[str]:
|
397
397
|
yield request.param
|
398
398
|
if os.getenv("FORWARDED_ALLOW_IPS"):
|
399
399
|
del os.environ["FORWARDED_ALLOW_IPS"]
|
@@ -25,7 +25,7 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
|
25
25
|
from uvicorn.middleware.wsgi import WSGIMiddleware
|
26
26
|
|
27
27
|
HTTPProtocolType = Literal["auto", "h11", "httptools"]
|
28
|
-
WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
|
28
|
+
WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
|
29
29
|
LifespanType = Literal["auto", "on", "off"]
|
30
30
|
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
|
31
31
|
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
|
@@ -47,6 +47,7 @@ WS_PROTOCOLS: dict[WSProtocolType, str | None] = {
|
|
47
47
|
"auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
|
48
48
|
"none": None,
|
49
49
|
"websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
|
50
|
+
"websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol",
|
50
51
|
"wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
|
51
52
|
}
|
52
53
|
LIFESPAN: dict[LifespanType, str] = {
|
@@ -313,7 +314,7 @@ class Config:
|
|
313
314
|
+ "directories, watching current working directory.",
|
314
315
|
reload_dirs,
|
315
316
|
)
|
316
|
-
self.reload_dirs = [Path
|
317
|
+
self.reload_dirs = [Path.cwd()]
|
317
318
|
|
318
319
|
logger.info(
|
319
320
|
"Will watch for changes in these directories: %s",
|
@@ -223,7 +223,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
223
223
|
"--proxy-headers/--no-proxy-headers",
|
224
224
|
is_flag=True,
|
225
225
|
default=True,
|
226
|
-
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For
|
226
|
+
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate url scheme and remote address info.",
|
227
227
|
)
|
228
228
|
@click.option(
|
229
229
|
"--server-header/--no-server-header",
|
@@ -1,9 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import
|
4
|
+
from typing import Callable
|
5
5
|
|
6
|
-
AutoWebSocketsProtocol:
|
6
|
+
AutoWebSocketsProtocol: Callable[..., asyncio.Protocol] | None
|
7
7
|
try:
|
8
8
|
import websockets # noqa
|
9
9
|
except ImportError: # pragma: no cover
|
@@ -34,6 +34,7 @@ from uvicorn.config import Config
|
|
34
34
|
from uvicorn.logging import TRACE_LOG_LEVEL
|
35
35
|
from uvicorn.protocols.utils import (
|
36
36
|
ClientDisconnected,
|
37
|
+
get_client_addr,
|
37
38
|
get_local_addr,
|
38
39
|
get_path_with_query_string,
|
39
40
|
get_remote_addr,
|
@@ -271,7 +272,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
271
272
|
message = cast("WebSocketAcceptEvent", message)
|
272
273
|
self.logger.info(
|
273
274
|
'%s - "WebSocket %s" [accepted]',
|
274
|
-
self.scope
|
275
|
+
get_client_addr(self.scope),
|
275
276
|
get_path_with_query_string(self.scope),
|
276
277
|
)
|
277
278
|
self.initial_response = None
|
@@ -289,7 +290,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
289
290
|
message = cast("WebSocketCloseEvent", message)
|
290
291
|
self.logger.info(
|
291
292
|
'%s - "WebSocket %s" 403',
|
292
|
-
self.scope
|
293
|
+
get_client_addr(self.scope),
|
293
294
|
get_path_with_query_string(self.scope),
|
294
295
|
)
|
295
296
|
self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"")
|
@@ -300,7 +301,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
300
301
|
message = cast("WebSocketResponseStartEvent", message)
|
301
302
|
self.logger.info(
|
302
303
|
'%s - "WebSocket %s" %d',
|
303
|
-
self.scope
|
304
|
+
get_client_addr(self.scope),
|
304
305
|
get_path_with_query_string(self.scope),
|
305
306
|
message["status"],
|
306
307
|
)
|
@@ -0,0 +1,417 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import sys
|
6
|
+
from asyncio.transports import BaseTransport, Transport
|
7
|
+
from http import HTTPStatus
|
8
|
+
from typing import Any, Literal, cast
|
9
|
+
from urllib.parse import unquote
|
10
|
+
|
11
|
+
from websockets.exceptions import InvalidState
|
12
|
+
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
|
13
|
+
from websockets.frames import Frame, Opcode
|
14
|
+
from websockets.http11 import Request
|
15
|
+
from websockets.server import ServerProtocol
|
16
|
+
|
17
|
+
from uvicorn._types import (
|
18
|
+
ASGIReceiveEvent,
|
19
|
+
ASGISendEvent,
|
20
|
+
WebSocketAcceptEvent,
|
21
|
+
WebSocketCloseEvent,
|
22
|
+
WebSocketResponseBodyEvent,
|
23
|
+
WebSocketResponseStartEvent,
|
24
|
+
WebSocketScope,
|
25
|
+
WebSocketSendEvent,
|
26
|
+
)
|
27
|
+
from uvicorn.config import Config
|
28
|
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
29
|
+
from uvicorn.protocols.utils import (
|
30
|
+
ClientDisconnected,
|
31
|
+
get_local_addr,
|
32
|
+
get_path_with_query_string,
|
33
|
+
get_remote_addr,
|
34
|
+
is_ssl,
|
35
|
+
)
|
36
|
+
from uvicorn.server import ServerState
|
37
|
+
|
38
|
+
if sys.version_info >= (3, 11): # pragma: no cover
|
39
|
+
from typing import assert_never
|
40
|
+
else: # pragma: no cover
|
41
|
+
from typing_extensions import assert_never
|
42
|
+
|
43
|
+
|
44
|
+
class WebSocketsSansIOProtocol(asyncio.Protocol):
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
config: Config,
|
48
|
+
server_state: ServerState,
|
49
|
+
app_state: dict[str, Any],
|
50
|
+
_loop: asyncio.AbstractEventLoop | None = None,
|
51
|
+
) -> None:
|
52
|
+
if not config.loaded:
|
53
|
+
config.load() # pragma: no cover
|
54
|
+
|
55
|
+
self.config = config
|
56
|
+
self.app = config.loaded_app
|
57
|
+
self.loop = _loop or asyncio.get_event_loop()
|
58
|
+
self.logger = logging.getLogger("uvicorn.error")
|
59
|
+
self.root_path = config.root_path
|
60
|
+
self.app_state = app_state
|
61
|
+
|
62
|
+
# Shared server state
|
63
|
+
self.connections = server_state.connections
|
64
|
+
self.tasks = server_state.tasks
|
65
|
+
self.default_headers = server_state.default_headers
|
66
|
+
|
67
|
+
# Connection state
|
68
|
+
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
69
|
+
self.server: tuple[str, int] | None = None
|
70
|
+
self.client: tuple[str, int] | None = None
|
71
|
+
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
|
72
|
+
|
73
|
+
# WebSocket state
|
74
|
+
self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue()
|
75
|
+
self.handshake_initiated = False
|
76
|
+
self.handshake_complete = False
|
77
|
+
self.close_sent = False
|
78
|
+
self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None
|
79
|
+
|
80
|
+
extensions = []
|
81
|
+
if self.config.ws_per_message_deflate:
|
82
|
+
extensions = [
|
83
|
+
ServerPerMessageDeflateFactory(
|
84
|
+
server_max_window_bits=12,
|
85
|
+
client_max_window_bits=12,
|
86
|
+
compress_settings={"memLevel": 5},
|
87
|
+
)
|
88
|
+
]
|
89
|
+
self.conn = ServerProtocol(
|
90
|
+
extensions=extensions,
|
91
|
+
max_size=self.config.ws_max_size,
|
92
|
+
logger=logging.getLogger("uvicorn.error"),
|
93
|
+
)
|
94
|
+
|
95
|
+
self.read_paused = False
|
96
|
+
self.writable = asyncio.Event()
|
97
|
+
self.writable.set()
|
98
|
+
|
99
|
+
# Buffers
|
100
|
+
self.bytes = b""
|
101
|
+
|
102
|
+
def connection_made(self, transport: BaseTransport) -> None:
|
103
|
+
"""Called when a connection is made."""
|
104
|
+
transport = cast(Transport, transport)
|
105
|
+
self.connections.add(self)
|
106
|
+
self.transport = transport
|
107
|
+
self.server = get_local_addr(transport)
|
108
|
+
self.client = get_remote_addr(transport)
|
109
|
+
self.scheme = "wss" if is_ssl(transport) else "ws"
|
110
|
+
|
111
|
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
112
|
+
prefix = "%s:%d - " % self.client if self.client else ""
|
113
|
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
|
114
|
+
|
115
|
+
def connection_lost(self, exc: Exception | None) -> None:
|
116
|
+
code = 1005 if self.handshake_complete else 1006
|
117
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
|
118
|
+
self.connections.remove(self)
|
119
|
+
|
120
|
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
121
|
+
prefix = "%s:%d - " % self.client if self.client else ""
|
122
|
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
|
123
|
+
|
124
|
+
self.handshake_complete = True
|
125
|
+
if exc is None:
|
126
|
+
self.transport.close()
|
127
|
+
|
128
|
+
def eof_received(self) -> None:
|
129
|
+
pass
|
130
|
+
|
131
|
+
def shutdown(self) -> None:
|
132
|
+
if self.handshake_complete:
|
133
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
|
134
|
+
self.conn.send_close(1012)
|
135
|
+
output = self.conn.data_to_send()
|
136
|
+
self.transport.write(b"".join(output))
|
137
|
+
else:
|
138
|
+
self.send_500_response()
|
139
|
+
self.transport.close()
|
140
|
+
|
141
|
+
def data_received(self, data: bytes) -> None:
|
142
|
+
self.conn.receive_data(data)
|
143
|
+
if self.conn.parser_exc is not None: # pragma: no cover
|
144
|
+
self.handle_parser_exception()
|
145
|
+
return
|
146
|
+
self.handle_events()
|
147
|
+
|
148
|
+
def handle_events(self) -> None:
|
149
|
+
for event in self.conn.events_received():
|
150
|
+
if isinstance(event, Request):
|
151
|
+
self.handle_connect(event)
|
152
|
+
if isinstance(event, Frame):
|
153
|
+
if event.opcode == Opcode.CONT:
|
154
|
+
self.handle_cont(event) # pragma: no cover
|
155
|
+
elif event.opcode == Opcode.TEXT:
|
156
|
+
self.handle_text(event)
|
157
|
+
elif event.opcode == Opcode.BINARY:
|
158
|
+
self.handle_bytes(event)
|
159
|
+
elif event.opcode == Opcode.PING:
|
160
|
+
self.handle_ping()
|
161
|
+
elif event.opcode == Opcode.PONG:
|
162
|
+
pass # pragma: no cover
|
163
|
+
elif event.opcode == Opcode.CLOSE:
|
164
|
+
self.handle_close(event)
|
165
|
+
else:
|
166
|
+
assert_never(event.opcode) # pragma: no cover
|
167
|
+
|
168
|
+
# Event handlers
|
169
|
+
|
170
|
+
def handle_connect(self, event: Request) -> None:
|
171
|
+
self.request = event
|
172
|
+
self.response = self.conn.accept(event)
|
173
|
+
self.handshake_initiated = True
|
174
|
+
if self.response.status_code != 101:
|
175
|
+
self.handshake_complete = True
|
176
|
+
self.close_sent = True
|
177
|
+
self.conn.send_response(self.response)
|
178
|
+
output = self.conn.data_to_send()
|
179
|
+
self.transport.write(b"".join(output))
|
180
|
+
self.transport.close()
|
181
|
+
return
|
182
|
+
|
183
|
+
headers = [
|
184
|
+
(key.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
|
185
|
+
for key, value in event.headers.raw_items()
|
186
|
+
]
|
187
|
+
raw_path, _, query_string = event.path.partition("?")
|
188
|
+
self.scope: WebSocketScope = {
|
189
|
+
"type": "websocket",
|
190
|
+
"asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
|
191
|
+
"http_version": "1.1",
|
192
|
+
"scheme": self.scheme,
|
193
|
+
"server": self.server,
|
194
|
+
"client": self.client,
|
195
|
+
"root_path": self.root_path,
|
196
|
+
"path": unquote(raw_path),
|
197
|
+
"raw_path": raw_path.encode("ascii"),
|
198
|
+
"query_string": query_string.encode("ascii"),
|
199
|
+
"headers": headers,
|
200
|
+
"subprotocols": event.headers.get_all("Sec-WebSocket-Protocol"),
|
201
|
+
"state": self.app_state.copy(),
|
202
|
+
"extensions": {"websocket.http.response": {}},
|
203
|
+
}
|
204
|
+
self.queue.put_nowait({"type": "websocket.connect"})
|
205
|
+
task = self.loop.create_task(self.run_asgi())
|
206
|
+
task.add_done_callback(self.on_task_complete)
|
207
|
+
self.tasks.add(task)
|
208
|
+
|
209
|
+
def handle_cont(self, event: Frame) -> None: # pragma: no cover
|
210
|
+
self.bytes += event.data
|
211
|
+
if event.fin:
|
212
|
+
self.send_receive_event_to_app()
|
213
|
+
|
214
|
+
def handle_text(self, event: Frame) -> None:
|
215
|
+
self.bytes = event.data
|
216
|
+
self.curr_msg_data_type: Literal["text", "bytes"] = "text"
|
217
|
+
if event.fin:
|
218
|
+
self.send_receive_event_to_app()
|
219
|
+
|
220
|
+
def handle_bytes(self, event: Frame) -> None:
|
221
|
+
self.bytes = event.data
|
222
|
+
self.curr_msg_data_type = "bytes"
|
223
|
+
if event.fin:
|
224
|
+
self.send_receive_event_to_app()
|
225
|
+
|
226
|
+
def send_receive_event_to_app(self) -> None:
|
227
|
+
if self.curr_msg_data_type == "text":
|
228
|
+
try:
|
229
|
+
self.queue.put_nowait({"type": "websocket.receive", "text": self.bytes.decode()})
|
230
|
+
except UnicodeDecodeError: # pragma: no cover
|
231
|
+
self.logger.exception("Invalid UTF-8 sequence received from client.")
|
232
|
+
self.conn.send_close(1007)
|
233
|
+
self.handle_parser_exception()
|
234
|
+
return
|
235
|
+
else:
|
236
|
+
self.queue.put_nowait({"type": "websocket.receive", "bytes": self.bytes})
|
237
|
+
if not self.read_paused:
|
238
|
+
self.read_paused = True
|
239
|
+
self.transport.pause_reading()
|
240
|
+
|
241
|
+
def handle_ping(self) -> None:
|
242
|
+
output = self.conn.data_to_send()
|
243
|
+
self.transport.write(b"".join(output))
|
244
|
+
|
245
|
+
def handle_close(self, event: Frame) -> None:
|
246
|
+
if not self.close_sent and not self.transport.is_closing():
|
247
|
+
assert self.conn.close_rcvd is not None
|
248
|
+
code = self.conn.close_rcvd.code
|
249
|
+
reason = self.conn.close_rcvd.reason
|
250
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
251
|
+
|
252
|
+
output = self.conn.data_to_send()
|
253
|
+
self.transport.write(b"".join(output))
|
254
|
+
self.transport.close()
|
255
|
+
|
256
|
+
def handle_parser_exception(self) -> None: # pragma: no cover
|
257
|
+
assert self.conn.close_sent is not None
|
258
|
+
code = self.conn.close_sent.code
|
259
|
+
reason = self.conn.close_sent.reason
|
260
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
261
|
+
|
262
|
+
output = self.conn.data_to_send()
|
263
|
+
self.transport.write(b"".join(output))
|
264
|
+
self.close_sent = True
|
265
|
+
self.transport.close()
|
266
|
+
|
267
|
+
def on_task_complete(self, task: asyncio.Task[None]) -> None:
|
268
|
+
self.tasks.discard(task)
|
269
|
+
|
270
|
+
async def run_asgi(self) -> None:
|
271
|
+
try:
|
272
|
+
result = await self.app(self.scope, self.receive, self.send)
|
273
|
+
except ClientDisconnected:
|
274
|
+
self.transport.close() # pragma: no cover
|
275
|
+
except BaseException:
|
276
|
+
self.logger.exception("Exception in ASGI application\n")
|
277
|
+
self.send_500_response()
|
278
|
+
self.transport.close()
|
279
|
+
else:
|
280
|
+
if not self.handshake_complete:
|
281
|
+
self.logger.error("ASGI callable returned without completing handshake.")
|
282
|
+
self.send_500_response()
|
283
|
+
self.transport.close()
|
284
|
+
elif result is not None:
|
285
|
+
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
|
286
|
+
self.transport.close()
|
287
|
+
|
288
|
+
def send_500_response(self) -> None:
|
289
|
+
if self.initial_response or self.handshake_complete:
|
290
|
+
return
|
291
|
+
response = self.conn.reject(500, "Internal Server Error")
|
292
|
+
self.conn.send_response(response)
|
293
|
+
output = self.conn.data_to_send()
|
294
|
+
self.transport.write(b"".join(output))
|
295
|
+
|
296
|
+
async def send(self, message: ASGISendEvent) -> None:
|
297
|
+
await self.writable.wait()
|
298
|
+
|
299
|
+
message_type = message["type"]
|
300
|
+
|
301
|
+
if not self.handshake_complete and self.initial_response is None:
|
302
|
+
if message_type == "websocket.accept":
|
303
|
+
message = cast(WebSocketAcceptEvent, message)
|
304
|
+
self.logger.info(
|
305
|
+
'%s - "WebSocket %s" [accepted]',
|
306
|
+
self.scope["client"],
|
307
|
+
get_path_with_query_string(self.scope),
|
308
|
+
)
|
309
|
+
headers = [
|
310
|
+
(name.decode("latin-1").lower(), value.decode("latin-1").lower())
|
311
|
+
for name, value in (self.default_headers + list(message.get("headers", [])))
|
312
|
+
]
|
313
|
+
accepted_subprotocol = message.get("subprotocol")
|
314
|
+
if accepted_subprotocol:
|
315
|
+
headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol))
|
316
|
+
self.response.headers.update(headers)
|
317
|
+
|
318
|
+
if not self.transport.is_closing():
|
319
|
+
self.handshake_complete = True
|
320
|
+
self.conn.send_response(self.response)
|
321
|
+
output = self.conn.data_to_send()
|
322
|
+
self.transport.write(b"".join(output))
|
323
|
+
|
324
|
+
elif message_type == "websocket.close":
|
325
|
+
message = cast(WebSocketCloseEvent, message)
|
326
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
327
|
+
self.logger.info(
|
328
|
+
'%s - "WebSocket %s" 403',
|
329
|
+
self.scope["client"],
|
330
|
+
get_path_with_query_string(self.scope),
|
331
|
+
)
|
332
|
+
response = self.conn.reject(HTTPStatus.FORBIDDEN, "")
|
333
|
+
self.conn.send_response(response)
|
334
|
+
output = self.conn.data_to_send()
|
335
|
+
self.close_sent = True
|
336
|
+
self.handshake_complete = True
|
337
|
+
self.transport.write(b"".join(output))
|
338
|
+
self.transport.close()
|
339
|
+
elif message_type == "websocket.http.response.start" and self.initial_response is None:
|
340
|
+
message = cast(WebSocketResponseStartEvent, message)
|
341
|
+
if not (100 <= message["status"] < 600):
|
342
|
+
raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"])
|
343
|
+
self.logger.info(
|
344
|
+
'%s - "WebSocket %s" %d',
|
345
|
+
self.scope["client"],
|
346
|
+
get_path_with_query_string(self.scope),
|
347
|
+
message["status"],
|
348
|
+
)
|
349
|
+
headers = [
|
350
|
+
(name.decode("latin-1"), value.decode("latin-1"))
|
351
|
+
for name, value in list(message.get("headers", []))
|
352
|
+
]
|
353
|
+
self.initial_response = (message["status"], headers, b"")
|
354
|
+
else:
|
355
|
+
msg = (
|
356
|
+
"Expected ASGI message 'websocket.accept', 'websocket.close' "
|
357
|
+
"or 'websocket.http.response.start' "
|
358
|
+
"but got '%s'."
|
359
|
+
)
|
360
|
+
raise RuntimeError(msg % message_type)
|
361
|
+
|
362
|
+
elif not self.close_sent and self.initial_response is None:
|
363
|
+
try:
|
364
|
+
if message_type == "websocket.send":
|
365
|
+
message = cast(WebSocketSendEvent, message)
|
366
|
+
bytes_data = message.get("bytes")
|
367
|
+
text_data = message.get("text")
|
368
|
+
if text_data:
|
369
|
+
self.conn.send_text(text_data.encode())
|
370
|
+
elif bytes_data:
|
371
|
+
self.conn.send_binary(bytes_data)
|
372
|
+
output = self.conn.data_to_send()
|
373
|
+
self.transport.write(b"".join(output))
|
374
|
+
|
375
|
+
elif message_type == "websocket.close" and not self.transport.is_closing():
|
376
|
+
message = cast(WebSocketCloseEvent, message)
|
377
|
+
code = message.get("code", 1000)
|
378
|
+
reason = message.get("reason", "") or ""
|
379
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
380
|
+
self.conn.send_close(code, reason)
|
381
|
+
output = self.conn.data_to_send()
|
382
|
+
self.transport.write(b"".join(output))
|
383
|
+
self.close_sent = True
|
384
|
+
self.transport.close()
|
385
|
+
else:
|
386
|
+
msg = "Expected ASGI message 'websocket.send' or 'websocket.close', but got '%s'."
|
387
|
+
raise RuntimeError(msg % message_type)
|
388
|
+
except InvalidState:
|
389
|
+
raise ClientDisconnected()
|
390
|
+
elif self.initial_response is not None:
|
391
|
+
if message_type == "websocket.http.response.body":
|
392
|
+
message = cast(WebSocketResponseBodyEvent, message)
|
393
|
+
body = self.initial_response[2] + message["body"]
|
394
|
+
self.initial_response = self.initial_response[:2] + (body,)
|
395
|
+
if not message.get("more_body", False):
|
396
|
+
response = self.conn.reject(self.initial_response[0], body.decode())
|
397
|
+
response.headers.update(self.initial_response[1])
|
398
|
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
399
|
+
self.conn.send_response(response)
|
400
|
+
output = self.conn.data_to_send()
|
401
|
+
self.close_sent = True
|
402
|
+
self.transport.write(b"".join(output))
|
403
|
+
self.transport.close()
|
404
|
+
else: # pragma: no cover
|
405
|
+
msg = "Expected ASGI message 'websocket.http.response.body' but got '%s'."
|
406
|
+
raise RuntimeError(msg % message_type)
|
407
|
+
|
408
|
+
else:
|
409
|
+
msg = "Unexpected ASGI message '%s', after sending 'websocket.close'."
|
410
|
+
raise RuntimeError(msg % message_type)
|
411
|
+
|
412
|
+
async def receive(self) -> ASGIReceiveEvent:
|
413
|
+
message = await self.queue.get()
|
414
|
+
if self.read_paused and self.queue.empty():
|
415
|
+
self.read_paused = False
|
416
|
+
self.transport.resume_reading()
|
417
|
+
return message
|
@@ -2,8 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
|
-
import
|
6
|
-
from typing import Literal, cast
|
5
|
+
from typing import Any, Literal, cast
|
7
6
|
from urllib.parse import unquote
|
8
7
|
|
9
8
|
import wsproto
|
@@ -27,6 +26,7 @@ from uvicorn.config import Config
|
|
27
26
|
from uvicorn.logging import TRACE_LOG_LEVEL
|
28
27
|
from uvicorn.protocols.utils import (
|
29
28
|
ClientDisconnected,
|
29
|
+
get_client_addr,
|
30
30
|
get_local_addr,
|
31
31
|
get_path_with_query_string,
|
32
32
|
get_remote_addr,
|
@@ -40,7 +40,7 @@ class WSProtocol(asyncio.Protocol):
|
|
40
40
|
self,
|
41
41
|
config: Config,
|
42
42
|
server_state: ServerState,
|
43
|
-
app_state: dict[str,
|
43
|
+
app_state: dict[str, Any],
|
44
44
|
_loop: asyncio.AbstractEventLoop | None = None,
|
45
45
|
) -> None:
|
46
46
|
if not config.loaded:
|
@@ -255,10 +255,10 @@ class WSProtocol(asyncio.Protocol):
|
|
255
255
|
|
256
256
|
if not self.handshake_complete:
|
257
257
|
if message_type == "websocket.accept":
|
258
|
-
message =
|
258
|
+
message = cast(WebSocketAcceptEvent, message)
|
259
259
|
self.logger.info(
|
260
260
|
'%s - "WebSocket %s" [accepted]',
|
261
|
-
self.scope
|
261
|
+
get_client_addr(self.scope),
|
262
262
|
get_path_with_query_string(self.scope),
|
263
263
|
)
|
264
264
|
subprotocol = message.get("subprotocol")
|
@@ -281,7 +281,7 @@ class WSProtocol(asyncio.Protocol):
|
|
281
281
|
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
282
282
|
self.logger.info(
|
283
283
|
'%s - "WebSocket %s" 403',
|
284
|
-
self.scope
|
284
|
+
get_client_addr(self.scope),
|
285
285
|
get_path_with_query_string(self.scope),
|
286
286
|
)
|
287
287
|
self.handshake_complete = True
|
@@ -292,14 +292,14 @@ class WSProtocol(asyncio.Protocol):
|
|
292
292
|
self.transport.close()
|
293
293
|
|
294
294
|
elif message_type == "websocket.http.response.start":
|
295
|
-
message =
|
295
|
+
message = cast(WebSocketResponseStartEvent, message)
|
296
296
|
# ensure status code is in the valid range
|
297
297
|
if not (100 <= message["status"] < 600):
|
298
298
|
msg = "Invalid HTTP status code '%d' in response."
|
299
299
|
raise RuntimeError(msg % message["status"])
|
300
300
|
self.logger.info(
|
301
301
|
'%s - "WebSocket %s" %d',
|
302
|
-
self.scope
|
302
|
+
get_client_addr(self.scope),
|
303
303
|
get_path_with_query_string(self.scope),
|
304
304
|
message["status"],
|
305
305
|
)
|
@@ -324,7 +324,7 @@ class WSProtocol(asyncio.Protocol):
|
|
324
324
|
elif not self.close_sent and not self.response_started:
|
325
325
|
try:
|
326
326
|
if message_type == "websocket.send":
|
327
|
-
message =
|
327
|
+
message = cast(WebSocketSendEvent, message)
|
328
328
|
bytes_data = message.get("bytes")
|
329
329
|
text_data = message.get("text")
|
330
330
|
data = text_data if bytes_data is None else bytes_data
|
@@ -333,7 +333,7 @@ class WSProtocol(asyncio.Protocol):
|
|
333
333
|
self.transport.write(output)
|
334
334
|
|
335
335
|
elif message_type == "websocket.close":
|
336
|
-
message =
|
336
|
+
message = cast(WebSocketCloseEvent, message)
|
337
337
|
self.close_sent = True
|
338
338
|
code = message.get("code", 1000)
|
339
339
|
reason = message.get("reason", "") or ""
|
@@ -350,7 +350,7 @@ class WSProtocol(asyncio.Protocol):
|
|
350
350
|
raise ClientDisconnected from exc
|
351
351
|
elif self.response_started:
|
352
352
|
if message_type == "websocket.http.response.body":
|
353
|
-
message =
|
353
|
+
message = cast("WebSocketResponseBodyEvent", message)
|
354
354
|
body_finished = not message.get("more_body", False)
|
355
355
|
reject_data = events.RejectData(data=message["body"], body_finished=body_finished)
|
356
356
|
output = self.conn.send(reject_data)
|
@@ -23,9 +23,10 @@ if TYPE_CHECKING:
|
|
23
23
|
from uvicorn.protocols.http.h11_impl import H11Protocol
|
24
24
|
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
25
25
|
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
26
|
+
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
|
26
27
|
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
27
28
|
|
28
|
-
Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol]
|
29
|
+
Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol, WebSocketsSansIOProtocol]
|
29
30
|
|
30
31
|
HANDLED_SIGNALS = (
|
31
32
|
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
@@ -63,10 +63,7 @@ class WatchFilesReload(BaseReload):
|
|
63
63
|
self.reloader_name = "WatchFiles"
|
64
64
|
self.reload_dirs = []
|
65
65
|
for directory in config.reload_dirs:
|
66
|
-
|
67
|
-
self.reload_dirs.append(directory)
|
68
|
-
if Path.cwd() not in self.reload_dirs:
|
69
|
-
self.reload_dirs.append(Path.cwd())
|
66
|
+
self.reload_dirs.append(directory)
|
70
67
|
|
71
68
|
self.watch_filter = FileFilter(config)
|
72
69
|
self.watcher = watch(
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|