emmett-framework / granian

A Rust HTTP server for Python applications

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ASGI Flow Error on websockets unidirectional pong while awaiting response

Antonyesk601 opened this issue · comments

Python Version: 3.10 and 3.11
granian version: 1.4.1
granian interface: asgi
FastAPI: 0.111.0

If an empty pong is sent from client as a keepalive heartbeat granian prints out a warning
[WARNING] Unsupported websocket message received Pong([])
If the pong is sent during awaiting input, granian raises ASGI flow error even when client is connected and that error can (and should) be ignored. The same code works without any modification using uvicorn

server.py

from fastapi import WebSocket, FastAPI
app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    data = await websocket.receive_bytes()
    await websocket.send_bytes(b"Message text was:"+data)

client.py

import websockets
import asyncio

async def main():
    uri = "ws://localhost:8000/ws"
    async with websockets.connect(uri) as websocket:
        await websocket.pong(b'')
        await asyncio.sleep(1)
        await websocket.send(b'')
        await asyncio.sleep(1)
            
asyncio.get_event_loop().run_until_complete(main())

Uvicorn trace when running with uvicorn server:app --host 0.0.0.0 --log-level debug

INFO:     Started server process [118726]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
DEBUG:    = connection is CONNECTING
DEBUG:    < GET /ws HTTP/1.1
DEBUG:    < host: localhost:8000
DEBUG:    < upgrade: websocket
DEBUG:    < connection: Upgrade
DEBUG:    < sec-websocket-key: Mj9N4bzT1G8E9fJ2o5w79w==
DEBUG:    < sec-websocket-version: 13
DEBUG:    < sec-websocket-extensions: permessage-deflate; client_max_window_bits
DEBUG:    < user-agent: Python/3.10 websockets/12.0
INFO:     ('127.0.0.1', 49668) - "WebSocket /ws" [accepted]
DEBUG:    > HTTP/1.1 101 Switching Protocols
DEBUG:    > Upgrade: websocket
DEBUG:    > Connection: Upgrade
DEBUG:    > Sec-WebSocket-Accept: BOjhd7UWFOiyW5vplYQicSPzVTw=
DEBUG:    > Sec-WebSocket-Extensions: permessage-deflate
DEBUG:    > date: Thu, 06 Jun 2024 16:55:24 GMT
DEBUG:    > server: uvicorn
INFO:     connection open
DEBUG:    = connection is OPEN
DEBUG:    < PONG '' [0 bytes]
DEBUG:    < BINARY  [0 bytes]
DEBUG:    > BINARY 4d 65 73 73 61 67 65 20 74 65 78 74 20 77 61 73 3a [17 bytes]
DEBUG:    < CLOSE 1000 (OK) [2 bytes]
DEBUG:    = connection is CLOSING
DEBUG:    > CLOSE 1000 (OK) [2 bytes]
DEBUG:    x half-closing TCP connection
DEBUG:    = connection is CLOSED
INFO:     connection closed

granian trace when running with granian --interface asgi server:app

[INFO] Starting granian (main PID: 118874)
[INFO] Listening at: http://127.0.0.1:8000
[INFO] Spawning worker-1 with pid: 118875
[INFO] Started worker-1
[INFO] Started worker-1 runtime-1
[WARNING] Unsupported websocket message received Pong([])
[ERROR] Application callable raised an exception
Traceback (most recent call last):
  File "/opt/conda/lib/python3.10/site-packages/granian/_futures.py", line 4, in future_watcher
    await inner(watcher.scope, watcher.proto)
  File "/opt/conda/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/errors.py", line 151, in __call__
    await self.app(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 373, in handle
    await self.app(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 96, in app
    await wrap_app_handling_exceptions(app, session)(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 94, in app
    await func(session)
  File "/opt/conda/lib/python3.10/site-packages/fastapi/routing.py", line 348, in app
    await dependant.call(**values)
  File "/home/antony/granianError/server.py", line 7, in websocket_endpoint
    data = await websocket.receive_bytes()
  File "/opt/conda/lib/python3.10/site-packages/starlette/websockets.py", line 146, in receive_bytes
    message = await self.receive()
  File "/opt/conda/lib/python3.10/site-packages/starlette/websockets.py", line 49, in receive
    message = await self._receive()
RuntimeError: ASGI flow error
[WARNING] Unsupported websocket message received Pong([])
[ERROR] Application callable raised an exception
Traceback (most recent call last):
  File "/opt/conda/lib/python3.10/site-packages/granian/_futures.py", line 4, in future_watcher
    await inner(watcher.scope, watcher.proto)
  File "/opt/conda/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/errors.py", line 151, in __call__
    await self.app(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 373, in handle
    await self.app(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 96, in app
    await wrap_app_handling_exceptions(app, session)(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 94, in app
    await func(session)
  File "/opt/conda/lib/python3.10/site-packages/fastapi/routing.py", line 348, in app
    await dependant.call(**values)
  File "/home/antony/granianError/server.py", line 7, in websocket_endpoint
    data = await websocket.receive_bytes()
  File "/opt/conda/lib/python3.10/site-packages/starlette/websockets.py", line 146, in receive_bytes
    message = await self.receive()
  File "/opt/conda/lib/python3.10/site-packages/starlette/websockets.py", line 49, in receive
    message = await self._receive()
RuntimeError: ASGI flow error
[WARNING] Unsupported websocket message received Pong([])
[ERROR] Application callable raised an exception
Traceback (most recent call last):
  File "/opt/conda/lib/python3.10/site-packages/granian/_futures.py", line 4, in future_watcher
    await inner(watcher.scope, watcher.proto)
  File "/opt/conda/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
    await super().__call__(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/applications.py", line 123, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/errors.py", line 151, in __call__
    await self.app(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 756, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 776, in app
    await route.handle(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 373, in handle
    await self.app(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 96, in app
    await wrap_app_handling_exceptions(app, session)(scope, receive, send)
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/opt/conda/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 94, in app
    await func(session)
  File "/opt/conda/lib/python3.10/site-packages/fastapi/routing.py", line 348, in app
    await dependant.call(**values)
  File "/home/antony/granianError/server.py", line 7, in websocket_endpoint
    data = await websocket.receive_bytes()
  File "/opt/conda/lib/python3.10/site-packages/starlette/websockets.py", line 146, in receive_bytes
    message = await self.receive()
  File "/opt/conda/lib/python3.10/site-packages/starlette/websockets.py", line 49, in receive
    message = await self._receive()
RuntimeError: ASGI flow error
[INFO] Shutting down granian
[INFO] Stopping worker-1 runtime-1
[INFO] Stopping worker-1

Workaround:
if server is running the receive message in a loop, put it in a try-except and check connection state if both websocket.client_state and websocket.application_state are connected then just continue with your normal loop with just a slight hiccup

Faced this when working with a .Net websockets client on Windows

@Antonyesk601 I agree Granian should ignore that, but why are you sending pongs without receiving pings?
I bet if you send pings everything is just fine.

Sending pings is absolutely fine
I wasn't intentionally doing so but the unidirectional pongs seem to be the default behaviour in .Net clients (a user of my api just happened to be using that) and kept reporting that his client keeps disconnecting and apparently thats how .Net works :"

@Antonyesk601 👍 got it. Gonna issue a patch version which discards pongs in the following days.