sanic-org / sanic

Accelerate your web app development | Build fast. Run fast.

Home Page:https://sanic.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Websocket invalid upgrade exception handling b0rkage

Tronic opened this issue · comments

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

A client apparently sent no Upgrade header to a websocket endpoint, leading to an error as it should. An ugly traceback is printed on terminal even though the error eventually gets handled correctly it would seem.

It would appear that the websockets module attempts to attach its exception on request._exception field which Sanic's Request doesn't have a slot for. This could be hidden if Sanic later used raise BadRequest(...) from None rather than raise SanicException(...), suppressing the chain and giving a non-500 error for what really is no server error. Not sure though if that would from this context ever reach the client anyway but at least it could avoid a traceback in server log.

If anyone wants to investigate and make a PR, feel free to (I am currently busy and cannot do that unfortunately).

Traceback (most recent call last):
  File "/home/user/.local/lib/python3.10/site-packages/websockets/server.py", line 111, in accept
    ) = self.process_request(request)
  File "/home/user/.local/lib/python3.10/site-packages/websockets/server.py", line 218, in process_request
    raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None)
websockets.exceptions.InvalidUpgrade: missing Upgrade header

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/sanic/sanic/server/protocols/websocket_protocol.py", line 120, in websocket_handshake
    resp: "http11.Response" = ws_proto.accept(request)
  File "/home/user/.local/lib/python3.10/site-packages/websockets/server.py", line 122, in accept
    request._exception = exc
AttributeError: 'Request' object has no attribute '_exception'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "handle_request", line 97, in handle_request
  File "/home/user/sanic/sanic/app.py", line 1047, in _websocket_handler
    ws = await protocol.websocket_handshake(request, subprotocols)
  File "/home/user/sanic/sanic/server/protocols/websocket_protocol.py", line 126, in websocket_handshake
    raise SanicException(msg, status_code=500)
sanic.exceptions.SanicException: Failed to open a WebSocket connection.
See server log for more information.

Code snippet

No response

Expected Behavior

400 Bad Request error reaching the client and being more silent on server side. Including the message of missing Upgrade header would be helpful for debugging (e.g. in case Nginx proxy config forgot to forward that header).

How do you run Sanic?

Sanic CLI

Operating System

Linux

Sanic Version

Almost 23.03.0 (a git version slightly before release)

Additional context

No response

commented

I plan to take a look at it if no one else started working on it.

Sanic server return a 500 error in this case. Should we make it be 400 instead? (I think so, but would like to double check)
Sorry misread the issue, and we should make it 400

FYI, this code snippet I got can reproduce the issue.

import http.client
import base64
import secrets
from urllib.parse import urlparse


def connect_websocket(url: str, message: str) -> None:
    parsed_url = urlparse(url)
    conn = http.client.HTTPConnection(parsed_url.netloc)

    websocket_key = base64.b64encode(secrets.token_bytes(16)).decode('utf-8')
    headers = {
        "Upgrade": "websocket",
        #"Connection": "Upgrade",
        "Sec-WebSocket-Key": websocket_key,
        "Sec-WebSocket-Version": "13"
    }

    conn.putrequest("GET", parsed_url.path)
    for header, value in headers.items():
        conn.putheader(header, value)
    conn.endheaders()

    response = conn.getresponse()
    if response.status == 101 and response.getheader('Upgrade', '').lower() == 'websocket':
        print("WebSocket handshake successful")
        conn.sock.sendall(message.encode())
    else:
        print(f"WebSocket handshake failed: {response.status} {response.reason}")

    conn.close()

websocket_url = "ws://localhost:8000/ws-echo"
connect_websocket(websocket_url, "Hello, WebSocket Server!")

Another way I am thinking about is to build WebSocket request object instead of Sanic Request in http protocol if the WebSocket handler is being used. This seems to come with a better performance, but much more complicated and might not be very feasible since we assume request is a Sanic Request object everywhere in the code base. I am still exploring. I would also like to hear feedback and comments about this idea.

Potentially there could be both WS and HTTP routes on the same path, e.g. if WS was considered its own method in routing (currently it counts as GET and conflicts with any other GET handler). This is currently not supported and doesn't really fit to how WS handlers are currently implemented internally.