Skip to content

Fix WebSocketResponse.prepared not correctly reflect the WebSocket's prepared state #10971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/6009.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``WebSocketResponse.prepared`` property to correctly reflect the prepared state, especially during timeout scenarios -- by :user:`bdraco`
4 changes: 4 additions & 0 deletions aiohttp/web_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ def can_prepare(self, request: BaseRequest) -> WebSocketReady:
else:
return WebSocketReady(True, protocol)

@property
def prepared(self) -> bool:
return self._writer is not None

@property
def closed(self) -> bool:
return self._closed
Expand Down
2 changes: 1 addition & 1 deletion tests/test_web_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,4 +670,4 @@ async def test_get_extra_info(
await ws.prepare(req)
ws._writer = ws_transport

assert ws.get_extra_info(valid_key, default_value) == expected_result
assert expected_result == ws.get_extra_info(valid_key, default_value)
113 changes: 113 additions & 0 deletions tests/test_web_websocket_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,3 +1332,116 @@ async def handler(request: web.Request) -> web.WebSocketResponse:
)
await client.server.close()
assert close_code == WSCloseCode.OK


async def test_websocket_prepare_timeout_close_issue(
loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient
) -> None:
"""Test that WebSocket can handle prepare with early returns.

This is a regression test for issue #6009 where the prepared property
incorrectly checked _payload_writer instead of _writer.
"""

async def handler(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse()
assert ws.can_prepare(request)
await ws.prepare(request)
await ws.send_str("test")
await ws.close()
return ws

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

# Connect via websocket
ws = await client.ws_connect("/ws")
msg = await ws.receive()
assert msg.type is WSMsgType.TEXT
assert msg.data == "test"
await ws.close()


async def test_websocket_prepare_timeout_from_issue_reproducer(
loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient
) -> None:
"""Test websocket behavior when prepare is interrupted.

This test verifies the fix for issue #6009 where close() would
fail after prepare() was interrupted.
"""
prepare_complete = asyncio.Event()
close_complete = asyncio.Event()

async def handler(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse()

# Prepare the websocket
await ws.prepare(request)
prepare_complete.set()

# Send a message to confirm connection works
await ws.send_str("connected")

# Wait for client to close
msg = await ws.receive()
assert msg.type is WSMsgType.CLOSE
await ws.close()
close_complete.set()

return ws

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

# Connect and verify the connection works
ws = await client.ws_connect("/ws")
await prepare_complete.wait()

msg = await ws.receive()
assert msg.type is WSMsgType.TEXT
assert msg.data == "connected"

# Close the connection
await ws.close()
await close_complete.wait()


async def test_websocket_prepared_property(
loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient
) -> None:
"""Test that WebSocketResponse.prepared property correctly reflects state."""
prepare_called = asyncio.Event()

async def handler(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse()

# Initially not prepared
initial_state = ws.prepared
assert not initial_state

# After prepare() is called, should be prepared
await ws.prepare(request)
prepare_called.set()

# Check prepared state
prepared_state = ws.prepared
assert prepared_state

# Send a message to verify the connection works
await ws.send_str("test")
await ws.close()
return ws

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

ws = await client.ws_connect("/")
await prepare_called.wait()
msg = await ws.receive()
assert msg.type is WSMsgType.TEXT
assert msg.data == "test"
await ws.close()
Loading