Skip to content

Commit 1ee187c

Browse files
authored
Fix WebSocketResponse.prepared not correctly reflect the WebSocket's prepared state (#10971)
1 parent bb5fc59 commit 1ee187c

File tree

4 files changed

+119
-1
lines changed

4 files changed

+119
-1
lines changed

CHANGES/6009.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed ``WebSocketResponse.prepared`` property to correctly reflect the prepared state, especially during timeout scenarios -- by :user:`bdraco`

aiohttp/web_ws.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ def can_prepare(self, request: BaseRequest) -> WebSocketReady:
358358
else:
359359
return WebSocketReady(True, protocol)
360360

361+
@property
362+
def prepared(self) -> bool:
363+
return self._writer is not None
364+
361365
@property
362366
def closed(self) -> bool:
363367
return self._closed

tests/test_web_websocket.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,4 +670,4 @@ async def test_get_extra_info(
670670
await ws.prepare(req)
671671
ws._writer = ws_transport
672672

673-
assert ws.get_extra_info(valid_key, default_value) == expected_result
673+
assert expected_result == ws.get_extra_info(valid_key, default_value)

tests/test_web_websocket_functional.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,3 +1332,116 @@ async def handler(request: web.Request) -> web.WebSocketResponse:
13321332
)
13331333
await client.server.close()
13341334
assert close_code == WSCloseCode.OK
1335+
1336+
1337+
async def test_websocket_prepare_timeout_close_issue(
1338+
loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient
1339+
) -> None:
1340+
"""Test that WebSocket can handle prepare with early returns.
1341+
1342+
This is a regression test for issue #6009 where the prepared property
1343+
incorrectly checked _payload_writer instead of _writer.
1344+
"""
1345+
1346+
async def handler(request: web.Request) -> web.WebSocketResponse:
1347+
ws = web.WebSocketResponse()
1348+
assert ws.can_prepare(request)
1349+
await ws.prepare(request)
1350+
await ws.send_str("test")
1351+
await ws.close()
1352+
return ws
1353+
1354+
app = web.Application()
1355+
app.router.add_route("GET", "/ws", handler)
1356+
client = await aiohttp_client(app)
1357+
1358+
# Connect via websocket
1359+
ws = await client.ws_connect("/ws")
1360+
msg = await ws.receive()
1361+
assert msg.type is WSMsgType.TEXT
1362+
assert msg.data == "test"
1363+
await ws.close()
1364+
1365+
1366+
async def test_websocket_prepare_timeout_from_issue_reproducer(
1367+
loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient
1368+
) -> None:
1369+
"""Test websocket behavior when prepare is interrupted.
1370+
1371+
This test verifies the fix for issue #6009 where close() would
1372+
fail after prepare() was interrupted.
1373+
"""
1374+
prepare_complete = asyncio.Event()
1375+
close_complete = asyncio.Event()
1376+
1377+
async def handler(request: web.Request) -> web.WebSocketResponse:
1378+
ws = web.WebSocketResponse()
1379+
1380+
# Prepare the websocket
1381+
await ws.prepare(request)
1382+
prepare_complete.set()
1383+
1384+
# Send a message to confirm connection works
1385+
await ws.send_str("connected")
1386+
1387+
# Wait for client to close
1388+
msg = await ws.receive()
1389+
assert msg.type is WSMsgType.CLOSE
1390+
await ws.close()
1391+
close_complete.set()
1392+
1393+
return ws
1394+
1395+
app = web.Application()
1396+
app.router.add_route("GET", "/ws", handler)
1397+
client = await aiohttp_client(app)
1398+
1399+
# Connect and verify the connection works
1400+
ws = await client.ws_connect("/ws")
1401+
await prepare_complete.wait()
1402+
1403+
msg = await ws.receive()
1404+
assert msg.type is WSMsgType.TEXT
1405+
assert msg.data == "connected"
1406+
1407+
# Close the connection
1408+
await ws.close()
1409+
await close_complete.wait()
1410+
1411+
1412+
async def test_websocket_prepared_property(
1413+
loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient
1414+
) -> None:
1415+
"""Test that WebSocketResponse.prepared property correctly reflects state."""
1416+
prepare_called = asyncio.Event()
1417+
1418+
async def handler(request: web.Request) -> web.WebSocketResponse:
1419+
ws = web.WebSocketResponse()
1420+
1421+
# Initially not prepared
1422+
initial_state = ws.prepared
1423+
assert not initial_state
1424+
1425+
# After prepare() is called, should be prepared
1426+
await ws.prepare(request)
1427+
prepare_called.set()
1428+
1429+
# Check prepared state
1430+
prepared_state = ws.prepared
1431+
assert prepared_state
1432+
1433+
# Send a message to verify the connection works
1434+
await ws.send_str("test")
1435+
await ws.close()
1436+
return ws
1437+
1438+
app = web.Application()
1439+
app.router.add_route("GET", "/", handler)
1440+
client = await aiohttp_client(app)
1441+
1442+
ws = await client.ws_connect("/")
1443+
await prepare_called.wait()
1444+
msg = await ws.receive()
1445+
assert msg.type is WSMsgType.TEXT
1446+
assert msg.data == "test"
1447+
await ws.close()

0 commit comments

Comments
 (0)