Skip to content

Handle asyncio.CancelledError when socket is closed without flushing and middleware in use#172

Merged
pgjones merged 1 commit into
pgjones:mainfrom
stopdropandrew:fix-cancelled-error-when-using-middleware
Dec 27, 2023
Merged

Handle asyncio.CancelledError when socket is closed without flushing and middleware in use#172
pgjones merged 1 commit into
pgjones:mainfrom
stopdropandrew:fix-cancelled-error-when-using-middleware

Conversation

@stopdropandrew

Copy link
Copy Markdown
Contributor

With the release of python 3.11.7 and using Starlette's BaseHTTPMiddleware, when a socket closes before finishing reading a response, I've noticed an asyncio.CancelledError being raised. This does not occur on 3.11.6.

Repro

from fastapi.responses import PlainTextResponse
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint


class ExampleMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
        return await call_next(request)


app = FastAPI()
app.add_middleware(ExampleMiddleware)


@app.get(
    "/health",
    response_class=PlainTextResponse,
)
def health():
    return "Healthy"

Script to exhibit behavior

import socket

connection = socket.create_connection(("127.0.0.1", 8000))
connection.sendall(b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n")
print(connection.recv(20).decode("utf8"))
connection.shutdown(socket.SHUT_RDWR)
connection.close()

Error

Exception in callback StreamReaderProtocol.connection_made.<locals>.callback(<Task cancell...io/run.py:90>>) at /Users/agrim/.pyenv/versions/3.11.7/lib/python3.11/asyncio/streams.py:248
handle: <Handle StreamReaderProtocol.connection_made.<locals>.callback(<Task cancell...io/run.py:90>>) at /Users/agrim/.pyenv/versions/3.11.7/lib/python3.11/asyncio/streams.py:248>
Traceback (most recent call last):
  File "/Users/agrim/.pyenv/versions/3.11.7/lib/python3.11/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/Users/agrim/.pyenv/versions/3.11.7/lib/python3.11/asyncio/streams.py", line 249, in callback
    exc = task.exception()
          ^^^^^^^^^^^^^^^^
  File "/Users/agrim/Library/Caches/pypoetry/virtualenvs/hypercorn-python-3-11-7-VsPAhN4z-py3.11/lib/python3.11/site-packages/hypercorn/asyncio/run.py", line 96, in _server_callback
    await TCPServer(app, loop, config, context, reader, writer)
  File "/Users/agrim/Library/Caches/pypoetry/virtualenvs/hypercorn-python-3-11-7-VsPAhN4z-py3.11/lib/python3.11/site-packages/hypercorn/asyncio/tcp_server.py", line 74, in run
    await self._close()
  File "/Users/agrim/Library/Caches/pypoetry/virtualenvs/hypercorn-python-3-11-7-VsPAhN4z-py3.11/lib/python3.11/site-packages/hypercorn/asyncio/tcp_server.py", line 117, in _close
    await self.writer.wait_closed()
  File "/Users/agrim/.pyenv/versions/3.11.7/lib/python3.11/asyncio/streams.py", line 361, in wait_closed
    await self._protocol._get_close_waiter(self)
asyncio.exceptions.CancelledError

Notes

  • I don't see this behavior with uvicorn. Also, if I switch to ASGIMiddleware I don't see this behavior.
  • I tried writing a test but don't have enough familiarity with hypercorn's tests.
  • I have a rudimentary idea of what's happening, but don't have enough familiarity with asyncio streams, starlette, and hypercorn interactions to know if this is the right solution. But it does stop the issue.

@shadchin

Copy link
Copy Markdown

I confirm that the patch helped me

@pgjones pgjones merged commit 1f874fc into pgjones:main Dec 27, 2023
@pgjones

pgjones commented Dec 27, 2023

Copy link
Copy Markdown
Owner

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants