encode / uvicorn

An ASGI web server, for Python. 🦄
https://www.uvicorn.org/
BSD 3-Clause "New" or "Revised" License
8.41k stars 728 forks source link

WebSocket does not complete coroutine after disconnection #2282

Closed carlos-rian-quartile closed 5 months ago

carlos-rian-quartile commented 6 months ago

Initial Checks

Discussion Link

https://github.com/tiangolo/fastapi/discussions/11244

Description

I started a discussion about this in the FastAPI project with @Kludex, and he asked me to open this task here and link the discussion link.

However, the problem I had was after FastAPI upgraded from 0.104.1 to 0.110.0.

My application did not pass the unit tests.

I´ve tried to use version 0.104.1, and working correctly, but 0.110.0 is not working.

In this example, using FastAPI, you can see that the CancelledError occurred.

Output error:

ERROR:root:error traceback: Traceback (most recent call last):
  File "/mnt/c/Users/laptop/Documents/01-repo/qd-chat/test.py", line 22, in my_websocket
    resp = await client.get(f"https://httpstat.us/200?sleep=5000")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpx/_client.py", line 1645, in _send_handling_auth
    response = await self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpx/_client.py", line 1719, in _send_single_request
    response = await transport.handle_async_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 366, in handle_async_request
    resp = await self._pool.handle_async_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 216, in handle_async_request
    raise exc from None
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 196, in handle_async_request
    response = await connection.handle_async_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 99, in handle_async_request
    raise exc
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 76, in handle_async_request
    stream = await self._connect(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 122, in _connect
    stream = await self._network_backend.connect_tcp(**kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/httpcore/_backends/anyio.py", line 114, in connect_tcp
    stream: anyio.abc.ByteStream = await anyio.connect_tcp(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/laptop/.cache/pypoetry/virtualenvs/qd-chat-vinvNfmc-py3.11/lib/python3.11/site-packages/anyio/_core/_sockets.py", line 192, in connect_tcp
    gai_res = await getaddrinfo(
              ^^^^^^^^^^^^^^^^^^
asyncio.exceptions.CancelledError

Example Code

import logging
import traceback

import httpx
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect

app = FastAPI()
logging.basicConfig(level=logging.DEBUG)

@app.websocket("/ws")
async def my_websocket(websocket: WebSocket, id: int = Query(...), name: str = Query(...)):
    try:
        await websocket.accept()
        while True:
            data = await websocket.receive_text()
            await websocket.send_text(f"Message text was: {data}")

    except WebSocketDisconnect:
        try:
            async with httpx.AsyncClient(timeout=10) as client:
                resp = await client.get(f"https://httpstat.us/200?sleep=5000")
                logging.warning(f"websocket disconnected - id: {id} - {resp}")
            logging.warning(f"websocket disconnected - id: {id}")  # not working
        except BaseException as e:
            logging.error(f"error type: {type(e)}")
            logging.error(f"error traceback: {traceback.format_exc()}")

from fastapi.testclient import TestClient

def test_websocket():
    client = TestClient(app)
    with client.websocket_connect("/ws?id=1&name=John") as websocket:
        data = "some data"
        websocket.send_text(data)
        msg = websocket.receive_text()
        assert msg == f"Message text was: {data}"

test_websocket()

Python, Uvicorn & OS Version

Python 3.11.3
FastAPI 0.110.0
Starlette 0.36.3
Running uvicorn 0.29.0 with CPython 3.11.3 on Linux

[!IMPORTANT]

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.

Fund with Polar

Kludex commented 5 months ago

You'll have to shield it.

With AnyIO you'll do as described here. With asyncio you'll do as described here.

carlos-rian-quartile commented 5 months ago

@Kludex

I tested using AnyIO, and it worked correctly. I appreciate your help.

import anyio

import logging

import httpx
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect

app = FastAPI()
logging.basicConfig(level=logging.DEBUG)

@app.websocket("/ws")
async def my_websocket(websocket: WebSocket, id: int = Query(...), name: str = Query(...)):
    try:
        await websocket.accept()
        while True:
            data = await websocket.receive_text()
            await websocket.send_text(f"Message text was: {data}")

    except WebSocketDisconnect:
        async with anyio.create_task_group() as _a:
            with anyio.CancelScope(shield=True) as _b:
                client = httpx.AsyncClient(timeout=10):
                resp = await client.get("https://httpstat.us/200?sleep=5000")
                logging.warning(f"websocket disconnected - id: {id} - {resp}")

        logging.warning(f"websocket disconnected - id: {id}")  # working

from fastapi.testclient import TestClient

def test_websocket():
    client = TestClient(app)
    with client.websocket_connect("/ws?id=1&name=John") as websocket:
        data = "some data"
        websocket.send_text(data)
        msg = websocket.receive_text()
        assert msg == f"Message text was: {data}"

test_websocket()