Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGES/14125.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Stopped the server WebSocket heartbeat from being rearmed after close:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
peer-close data arriving after the close handshake scheduled a deferred
heartbeat reset that re-created the heartbeat timer on the closed connection.
The deferred reset is now dropped when the connection is closing or closed --
by :user:`rinopatrick`.
1 change: 1 addition & 0 deletions CHANGES/14236.bugfix.rst
9 changes: 8 additions & 1 deletion aiohttp/web_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def __bool__(self) -> bool:


class WebSocketResponse(StreamResponse, Generic[_DecodeText]):

_length_check: bool = False
_ws_protocol: str | None = None
_writer: WebSocketWriter | None = None
Expand Down Expand Up @@ -153,12 +152,20 @@ def _flush_heartbeat_reset(self) -> None:
self._heartbeat_reset_handle = None
if not self._need_heartbeat_reset:
return
if self._closed or self._closing:
# The connection closed while the reset was deferred (e.g. the
# peer close frame arrived after the close handshake). Do not
# re-arm the heartbeat on a dead connection (see #14125).
self._need_heartbeat_reset = False
return
self._reset_heartbeat()
self._need_heartbeat_reset = False

def _reset_heartbeat(self) -> None:
if self._heartbeat is None:
return
if self._closed or self._closing:
return
self._cancel_pong_response_cb()
req = self._req
timeout_ceil_threshold = (
Expand Down
16 changes: 16 additions & 0 deletions tests/test_web_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ async def test_flush_heartbeat_reset_returns_early_when_not_needed() -> None:
reset.assert_not_called()


async def test_flush_heartbeat_reset_does_not_rearm_after_close() -> None:
# Peer-close data arriving after the close handshake must not re-arm
# the heartbeat on the closed connection (see #14125).
ws = web.WebSocketResponse(heartbeat=10)
ws._loop = asyncio.get_running_loop()
ws._set_closed()

# data (e.g. the peer close frame) arrives after close
ws._on_data_received()
await asyncio.sleep(0)
await asyncio.sleep(0)

assert ws._heartbeat_cb is None
assert ws._need_heartbeat_reset is False


async def test_send_heartbeat_returns_early_when_reset_is_pending() -> None:
ws = web.WebSocketResponse(heartbeat=0.05)
ws._need_heartbeat_reset = True
Expand Down
Loading