Skip to content

Commit ecc2177

Browse files
agnersclaude
andcommitted
Raise ConnectionResetError if transport is None
When a client disconnects immediately after connecting, the transport might become None. Replace the assert with a ConnectionResetError, which is consistent with the existing aiohttp exception hierarchy and the documented guidance to handle OSError for peer disconnection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 601bedb commit ecc2177

5 files changed

Lines changed: 71 additions & 3 deletions

File tree

CHANGES/11761.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed ``AssertionError`` when the transport is ``None`` during WebSocket
2+
preparation or file response sending (e.g. when a client disconnects
3+
immediately after connecting). A ``ConnectionResetError`` is now raised
4+
instead -- by :user:`agners`.

aiohttp/web_fileresponse.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ async def _sendfile(
128128

129129
loop = request._loop
130130
transport = request.transport
131-
assert transport is not None
131+
if transport is None:
132+
raise ConnectionResetError("Transport is not available")
132133

133134
try:
134135
await loop.sendfile(transport, fobj, offset, count)

aiohttp/web_ws.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,8 @@ def _pre_start(self, request: BaseRequest) -> tuple[str | None, WebSocketWriter]
361361
self.force_close()
362362
self._compress = compress
363363
transport = request._protocol.transport
364-
assert transport is not None
364+
if transport is None:
365+
raise ConnectionResetError("Transport is not available")
365366
writer = WebSocketWriter(
366367
request._protocol,
367368
transport,

tests/test_web_websocket.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,3 +772,12 @@ async def test_get_extra_info(
772772
ws._writer = ws_transport
773773

774774
assert expected_result == ws.get_extra_info(valid_key, default_value)
775+
776+
777+
async def test_prepare_transport_not_available(make_request: _RequestMaker) -> None:
778+
req = make_request("GET", "/")
779+
ws = web.WebSocketResponse()
780+
# Simulate transport being None
781+
req._protocol.transport = None
782+
with pytest.raises(ConnectionResetError, match="Transport is not available"):
783+
await ws.prepare(req)

tests/test_web_websocket_functional.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212

1313
import aiohttp
14-
from aiohttp import WSServerHandshakeError, web
14+
from aiohttp import WSServerHandshakeError, hdrs, web
1515
from aiohttp.http import WSCloseCode, WSMsgType
1616
from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer
1717

@@ -1659,3 +1659,56 @@ async def websocket_handler(
16591659
assert msg.type is aiohttp.WSMsgType.TEXT
16601660
assert msg.data == "success"
16611661
await ws.close()
1662+
1663+
1664+
async def test_prepare_after_client_disconnect(aiohttp_client: AiohttpClient) -> None:
1665+
"""Test ConnectionResetError when client disconnects before ws.prepare().
1666+
1667+
Reproduces the race condition where:
1668+
- Client connects and sends a WebSocket upgrade request
1669+
- Handler starts async work (e.g. authentication) before calling ws.prepare()
1670+
- Client disconnects while the handler is busy
1671+
- Handler then calls ws.prepare() → ConnectionResetError (not AssertionError)
1672+
"""
1673+
handler_started = asyncio.Event()
1674+
captured_protocol = None
1675+
1676+
async def handler(request: web.Request) -> web.Response:
1677+
nonlocal captured_protocol
1678+
ws = web.WebSocketResponse()
1679+
captured_protocol = request._protocol
1680+
handler_started.set()
1681+
# Simulate async work (e.g., auth check) during which client disconnects.
1682+
await asyncio.sleep(0)
1683+
with pytest.raises(ConnectionResetError, match="Transport is not available"):
1684+
await ws.prepare(request)
1685+
return web.Response(status=503)
1686+
1687+
app = web.Application()
1688+
app.router.add_route("GET", "/", handler)
1689+
client = await aiohttp_client(app)
1690+
1691+
request_task = asyncio.create_task(
1692+
client.session.get(
1693+
client.make_url("/"),
1694+
headers={
1695+
hdrs.UPGRADE: "websocket",
1696+
hdrs.CONNECTION: "Upgrade",
1697+
hdrs.SEC_WEBSOCKET_KEY: "dGhlIHNhbXBsZSBub25jZQ==",
1698+
hdrs.SEC_WEBSOCKET_VERSION: "13",
1699+
},
1700+
)
1701+
)
1702+
1703+
# Wait until the handler is running but has not yet called ws.prepare().
1704+
await handler_started.wait()
1705+
assert captured_protocol is not None
1706+
1707+
# Simulate the client disconnecting abruptly.
1708+
captured_protocol.force_close()
1709+
1710+
# Yield so the handler can resume and hit the ConnectionResetError.
1711+
await asyncio.sleep(0)
1712+
1713+
with contextlib.suppress(aiohttp.ServerDisconnectedError, aiohttp.ClientConnectionResetError):
1714+
await request_task

0 commit comments

Comments
 (0)