Skip to content
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

Leave websocket transport open if receive times out or is cancelled #8251

Merged
merged 10 commits into from
Mar 29, 2024
4 changes: 4 additions & 0 deletions CHANGES/8251.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Leave websocket transport open if receive times out or is cancelled
-- by :user:`bdraco`.

This restores the behavior prior to the change in #7978.
3 changes: 1 addition & 2 deletions aiohttp/web_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,7 @@ async def receive(self, timeout: Optional[float] = None) -> WSMessage:
waiter = self._waiting
set_result(waiter, True)
self._waiting = None
except (asyncio.CancelledError, asyncio.TimeoutError):
self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE)
except asyncio.TimeoutError:
raise
except EofStream:
self._close_code = WSCloseCode.OK
Expand Down
3 changes: 2 additions & 1 deletion tests/test_web_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,8 @@ async def test_receive_timeouterror(make_request: Any, loop: Any) -> None:
with pytest.raises(asyncio.TimeoutError):
await ws.receive()

assert len(ws._req.transport.close.mock_calls) == 1
# Should not close the connection on timeout
assert len(ws._req.transport.close.mock_calls) == 0


async def test_multiple_receive_on_close_connection(make_request: Any) -> None:
Expand Down
93 changes: 93 additions & 0 deletions tests/test_web_websocket_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# HTTP websocket server functional tests

import asyncio
import contextlib
import sys
from typing import Any, Optional

import pytest
Expand Down Expand Up @@ -828,3 +830,94 @@ async def ws_handler(request):
resp = await client.get("/api/null", timeout=aiohttp.ClientTimeout(total=1))
assert (await resp.json()) == {"err": None}
resp.close()


async def test_receive_being_cancelled_keeps_connection_open(
loop: Any, aiohttp_client: Any
) -> None:
closed = loop.create_future()

async def handler(request):
ws = web.WebSocketResponse(autoping=False)
await ws.prepare(request)

task = asyncio.create_task(ws.receive())
await asyncio.sleep(0)
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
Dismissed Show dismissed Hide dismissed

msg = await ws.receive()
assert msg.type == WSMsgType.PING
await asyncio.sleep(0)
await ws.pong("data")

msg = await ws.receive()
assert msg.type == WSMsgType.CLOSE
assert msg.data == WSCloseCode.OK
assert msg.extra == "exit message"
closed.set_result(None)
return ws

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

ws = await client.ws_connect("/", autoping=False)

await asyncio.sleep(0)
await ws.ping("data")

msg = await ws.receive()
assert msg.type == WSMsgType.PONG
assert msg.data == b"data"

await ws.close(code=WSCloseCode.OK, message="exit message")

await closed
Dismissed Show dismissed Hide dismissed


async def test_receive_timeout_keeps_connection_open(
loop: Any, aiohttp_client: Any
) -> None:
closed = loop.create_future()
timed_out = loop.create_future()

async def handler(request):
ws = web.WebSocketResponse(autoping=False)
await ws.prepare(request)

task = asyncio.create_task(ws.receive(sys.float_info.min))
with contextlib.suppress(asyncio.TimeoutError):
await task
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed

timed_out.set_result(None)

msg = await ws.receive()
assert msg.type == WSMsgType.PING
await asyncio.sleep(0)
await ws.pong("data")

msg = await ws.receive()
assert msg.type == WSMsgType.CLOSE
assert msg.data == WSCloseCode.OK
assert msg.extra == "exit message"
closed.set_result(None)
return ws

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

ws = await client.ws_connect("/", autoping=False)

await timed_out
Dismissed Show dismissed Hide dismissed
await ws.ping("data")

msg = await ws.receive()
assert msg.type == WSMsgType.PONG
assert msg.data == b"data"

await ws.close(code=WSCloseCode.OK, message="exit message")

await closed
Dismissed Show dismissed Hide dismissed
Loading