Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGES/11761.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed ``AssertionError`` when the transport is ``None`` during WebSocket
preparation or file response sending (e.g. when a client disconnects
immediately after connecting). A ``ConnectionResetError`` is now raised
instead -- by :user:`agners`.
3 changes: 2 additions & 1 deletion aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ async def _sendfile(

loop = request._loop
transport = request.transport
assert transport is not None
if transport is None:
raise ConnectionResetError("Connection lost")

try:
await loop.sendfile(transport, fobj, offset, count)
Expand Down
3 changes: 2 additions & 1 deletion aiohttp/web_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,8 @@ def _pre_start(self, request: BaseRequest) -> tuple[str | None, WebSocketWriter]
self.force_close()
self._compress = compress
transport = request._protocol.transport
assert transport is not None
if transport is None:
raise ConnectionResetError("Connection lost")
writer = WebSocketWriter(
request._protocol,
transport,
Expand Down
57 changes: 56 additions & 1 deletion tests/test_web_websocket_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest

import aiohttp
from aiohttp import WSServerHandshakeError, web
from aiohttp import WSServerHandshakeError, hdrs, web
from aiohttp.http import WSCloseCode, WSMsgType
from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer

Expand Down Expand Up @@ -1661,3 +1661,58 @@ async def websocket_handler(
assert msg.type is aiohttp.WSMsgType.TEXT
assert msg.data == "success"
await ws.close()


async def test_prepare_after_client_disconnect(aiohttp_client: AiohttpClient) -> None:
"""Test ConnectionResetError when client disconnects before ws.prepare().

Reproduces the race condition where:
- Client connects and sends a WebSocket upgrade request
- Handler starts async work (e.g. authentication) before calling ws.prepare()
- Client disconnects while the handler is busy
- Handler then calls ws.prepare() → ConnectionResetError (not AssertionError)
"""
handler_started = asyncio.Event()
captured_protocol = None

async def handler(request: web.Request) -> web.Response:
nonlocal captured_protocol
ws = web.WebSocketResponse()
captured_protocol = request._protocol
handler_started.set()
# Simulate async work (e.g., auth check) during which client disconnects.
await asyncio.sleep(0)
with pytest.raises(ConnectionResetError, match="Connection lost"):
await ws.prepare(request)
return web.Response(status=503)

app = web.Application()
app.router.add_route("GET", "/", handler)
client = await aiohttp_client(app)

request_task = asyncio.create_task(
client.session.get(
client.make_url("/"),
headers={
hdrs.UPGRADE: "websocket",
hdrs.CONNECTION: "Upgrade",
hdrs.SEC_WEBSOCKET_KEY: "dGhlIHNhbXBsZSBub25jZQ==",
hdrs.SEC_WEBSOCKET_VERSION: "13",
},
)
)

# Wait until the handler is running but has not yet called ws.prepare().
await handler_started.wait()
assert captured_protocol is not None

# Simulate the client disconnecting abruptly.
captured_protocol.force_close()

# Yield so the handler can resume and hit the ConnectionResetError.
await asyncio.sleep(0)

with contextlib.suppress(
aiohttp.ServerDisconnectedError, aiohttp.ClientConnectionResetError
):
await request_task
Loading