emmett-framework / granian

A Rust HTTP server for Python applications
BSD 3-Clause "New" or "Revised" License
2.9k stars 86 forks source link

[BUG] ASGI websockets behavior websocket.connect and websocket.accept #18

Closed cirospaciari closed 1 year ago

cirospaciari commented 1 year ago

Discussed in https://github.com/emmett-framework/granian/discussions/17

Originally posted by **cirospaciari** December 1, 2022 Just testing the websockets ASGI protocol behavior, in uvicorn and other the following code i used ```python async def app(scope, receive, send): # handle non websocket if scope['type'] == 'http': await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': b'Connect via ws protocol!', }) if scope['type'] != 'websocket': return protocols = scope.get('subprotocols', []) scope = await receive() # get connection assert scope['type'] == 'websocket.connect' # accept connection await send({ 'type': 'websocket.accept', 'subprotocol': protocols[0] if len(protocols) > 0 else None }) # get data while True: scope = await receive() type = scope['type'] # disconnected! if type == 'websocket.disconnect': print("disconnected!", scope) break # echo! await send({ 'type': 'websocket.send', 'bytes': scope.get('bytes', None), 'text': scope.get('text', '') }) ``` The order: scope['type'] = websocket: call await receive() to get the first websocket.connect send websocket.accept to accept the connection call await receive() to get websocket.receive or websocket.disconnect send websocket.close if i want to end the connection In granian ASGI: ```python async def app(scope, receive, send): # handle non websocket if scope['type'] == 'http': await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': b'Connect via ws protocol!', }) if scope['type'] != 'websocket': return protocols = scope.get('subprotocols', []) # accept connection await send({ 'type': 'websocket.accept', 'subprotocol': protocols[0] if len(protocols) > 0 else None }) scope = await receive() assert scope['type'] == 'websocket.connect' # get data while True: scope = await receive() type = scope['type'] # disconnected! if type == 'websocket.disconnect': print("disconnected!", scope) break # echo! await send({ 'type': 'websocket.send', 'bytes': scope.get('bytes', None), 'text': scope.get('text', '') }) ``` The order: scope['type'] = websocket: send websocket.accept to accept the connection call await receive() to get the first websocket.connect call await receive() to get websocket.receive or websocket.disconnect send websocket.close if i want to end the connection Falcon source code for ASGI for reference: ```python async def _handle_websocket(self, ver, scope, receive, send): first_event = await receive() if first_event['type'] != EventType.WS_CONNECT: # NOTE(kgriffs): The handshake was abandoned or this is a message # we don't support, so bail out. This also fulfills the ASGI # spec requirement to only process the request after # receiving and verifying the first event. await send({'type': EventType.WS_CLOSE, 'code': WSCloseCode.SERVER_ERROR}) return ``` https://github.com/falconry/falcon/blob/master/falcon/asgi/app.py Line 970 ASGI spec about websocket.connect >"This message must be responded to with either an Accept message or a Close message before the socket will pass websocket.receive messages. The protocol server must send this message during the handshake phase of the WebSocket and not complete the handshake until it gets a reply, returning HTTP status code 403 if the connection is denied." https://asgi.readthedocs.io/en/latest/specs/www.html#connect-receive-event Another thing is if i call in granian ```python await send({ 'type': 'websocket.send', 'bytes': None, 'text': 'something' }) ``` It will send an empty binary message instead of using the 'text', in uvicorn the 'text' is sent if bytes is None In ASGI docs: >"Exactly one of bytes or text must be non-None. One or both keys may be present, however." https://asgi.readthedocs.io/en/latest/specs/www.html#send-send-event
gi0baro commented 1 year ago

This is now tracked separately in #19 and #20