Closed gangstand closed 1 week ago
The same issue occurs when using Granian as the ASGI server. When I run the application with Granian, I encounter the same MethodNotAllowedException related to sending a 'GET' method when attempting to establish a WebSocket connection. Here are the logs from Granian:
granian --interface asgi main:litestar_app
[INFO] Starting granian (main PID: 15764)
[INFO] Listening at: http://127.0.0.1:8000
[INFO] Spawning worker-1 with pid: 15292
[INFO] Started worker-1
[INFO] Started worker-1 runtime-1
ERROR - 2024-10-25 11:37:30,842 - litestar - config - Uncaught exception (connection_type=websocket, path=/):
Traceback (most recent call last):
File "C:\Users\gangstand\Desktop\ws.chat\.venv\Lib\site-packages\litestar\_asgi\routing_trie\traversal.py", line 136, in parse_path_to_route
asgi_app, handler = parse_node_handlers(node=root_node.children[path], method=method)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\gangstand\Desktop\ws.chat\.venv\Lib\site-packages\litestar\_asgi\routing_trie\traversal.py", line 82, in parse_node_handlers
return node.asgi_handlers[method]
~~~~~~~~~~~~~~~~~~^^^^^^^^
KeyError: 'GET'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\gangstand\Desktop\ws.chat\.venv\Lib\site-packages\litestar\middleware\_internal\exceptions\middleware.py", line 159, in __call__
await self.app(scope, receive, capture_response_started)
File "C:\Users\gangstand\Desktop\ws.chat\.venv\Lib\site-packages\litestar\_asgi\asgi_router.py", line 90, in __call__
asgi_app, route_handler, scope["path"], scope["path_params"], path_template = self.handle_routing(
^^^^^^^^^^^^^^^^^^^^
File "C:\Users\gangstand\Desktop\ws.chat\.venv\Lib\site-packages\litestar\_asgi\asgi_router.py", line 115, in handle_routing
return parse_path_to_route(
^^^^^^^^^^^^^^^^^^^^
File "C:\Users\gangstand\Desktop\ws.chat\.venv\Lib\site-packages\litestar\_asgi\routing_trie\traversal.py", line 173, in parse_path_to_route
raise MethodNotAllowedException() from e
litestar.exceptions.http_exceptions.MethodNotAllowedException: 405: Method Not Allowed
Thus, the problem is reproducible with Granian as well, where the WebSocket connection sends a 'GET' method instead of the expected None.
According to RFC 6455, which defines the WebSocket protocol, the connection is indeed initiated with an HTTP GET
request.
This initial HTTP request is used to establish the connection and includes specific headers required to successfully switch to the WebSocket protocol.
The handshake from the client looks as follows:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
The handshake from the server looks as follows:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
The leading line from the client follows the Request-Line format.
The leading line from the server follows the Status-Line format. The
Request-Line and Status-Line productions are defined in RFC2616 (https://datatracker.ietf.org/doc/html/rfc2616).
Please correct me if I'm wrong.
I think the bug lies with socketify / granian here. If you take a look at the ASGI specs, the websocket
scope does not define a method
key: https://asgi.readthedocs.io/en/latest/specs/www.html#websocket-connection-scope.
The RFC you are referring to is the initial request, which isn't handled by the ASGI app, but the ASGI server.
Description
When using Litestar with Socketify as the ASGI server for handling WebSocket connections, I encountered a MethodNotAllowedException with the following traceback. The error seems to stem from the fact that Socketify is sending a 'GET' method in the ASGI scope, whereas Litestar expects the method to be None for WebSocket connections.
Additional Information: Upon investigation, it seems that Socketify is passing 'GET' in the ASGI scope for WebSocket upgrades. However, Litestar expects the method to be None for WebSocket connections. The code for the ASGI implementation in Socketify at line 106 shows that the method is being set to 'GET'.
https://github.com/cirospaciari/socketify.py/blob/main/src/socketify/asgi.py
"method": ffi.unpack(info.method, info.method_size).decode("utf8"),
It would be helpful if Litestar could gracefully handle this scenario or if Socketify could adjust its ASGI scope generation for WebSocket connections to comply with the expected behavior.
URL to code causing the issue
https://github.com/litestar-org/litestar/blob/main/litestar/_asgi/routing_trie/traversal.py
MCVE
Steps to reproduce
Screenshots
No response
Logs
Litestar Version
[tool.poetry] name = "app" version = "0.1.0" description = "WebSocket connection fails due to 'GET' method being sent instead of None (Litestar expects None)" authors = ["gangstand ganggstand@gmail.com"]
[tool.poetry.dependencies] python = "^3.12" socketify = "^0.0.28" litestar = "^2.12.1" granian = "^1.6.1" uvicorn = "^0.32.0" websockets = "^13.1"
[tool.poetry.dev-dependencies] ruff = "" isort = "" mypy = "*"
[build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"
[tool.ruff] fix = true unsafe-fixes = true line-length = 120
[tool.ruff.format] docstring-code-format = true
[tool.ruff.lint] select = ["ALL"] ignore = ["EM", "FBT", "TRY003", "D1", "D203", "D213", "G004", "FA", "COM812", "ISC001", "PLR0913"]
[tool.ruff.lint.isort] no-lines-before = ["standard-library", "local-folder"] known-third-party = [] known-local-folder = [] lines-after-imports = 2
[tool.ruff.lint.extend-per-file-ignores] "tests/*.py" = ["S101", "S311"]
[tool.coverage.report] exclude_also = ["if typing.TYPE_CHECKING:"]
Platform