Skip to content

Commit 74d9355

Browse files
bdracolenard-mosys
andauthored
[PR #9508/274c54e backport][3.11] Discard non-close messages in close timeout window (#9688)
Co-authored-by: pre-commit-ci[bot] Co-authored-by: J. Nick Koston <[email protected]> Co-authored-by: lenard-mosys <[email protected]>
1 parent 49f65e6 commit 74d9355

File tree

4 files changed

+79
-10
lines changed

4 files changed

+79
-10
lines changed

CHANGES/9506.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed :py:meth:`WebSocketResponse.close() <aiohttp.web.WebSocketResponse.close>` to discard non-close messages within its timeout window after sending close -- by :user:`lenard-mosys`.

CONTRIBUTORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ Lubomir Gelo
219219
Ludovic Gasc
220220
Luis Pedrosa
221221
Lukasz Marcin Dobrzanski
222+
Lénárd Szolnoki
222223
Makc Belousow
223224
Manuel Miranda
224225
Marat Sharafutdinov

aiohttp/web_ws.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,11 @@ async def close(
473473

474474
try:
475475
async with async_timeout.timeout(self._timeout):
476-
msg = await reader.read()
476+
while True:
477+
msg = await reader.read()
478+
if msg.type is WSMsgType.CLOSE:
479+
self._set_code_close_transport(msg.data)
480+
return True
477481
except asyncio.CancelledError:
478482
self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE)
479483
raise
@@ -482,14 +486,6 @@ async def close(
482486
self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE)
483487
return True
484488

485-
if msg.type is WSMsgType.CLOSE:
486-
self._set_code_close_transport(msg.data)
487-
return True
488-
489-
self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE)
490-
self._exception = asyncio.TimeoutError()
491-
return True
492-
493489
def _set_closing(self, code: WSCloseCode) -> None:
494490
"""Set the close code and mark the connection as closing."""
495491
self._closing = True

tests/test_web_websocket_functional.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1192,7 +1192,11 @@ async def test_abnormal_closure_when_server_does_not_receive(
11921192
"""Test abnormal closure when the server closes and a message is pending."""
11931193

11941194
async def handler(request: web.Request) -> web.WebSocketResponse:
1195-
ws = web.WebSocketResponse()
1195+
# Setting close timeout to 0, otherwise the server waits for a
1196+
# close response for 10 seconds by default.
1197+
# This would make the client's autoclose in resp.receive() to succeed,
1198+
# closing the connection cleanly from both sides.
1199+
ws = web.WebSocketResponse(timeout=0)
11961200
await ws.prepare(request)
11971201
await ws.close()
11981202
return ws
@@ -1206,3 +1210,70 @@ async def handler(request: web.Request) -> web.WebSocketResponse:
12061210
msg = await resp.receive()
12071211
assert msg.type is aiohttp.WSMsgType.CLOSE
12081212
assert resp.close_code == WSCloseCode.ABNORMAL_CLOSURE
1213+
1214+
1215+
async def test_abnormal_closure_when_client_does_not_close(
1216+
aiohttp_client: AiohttpClient,
1217+
) -> None:
1218+
"""Test abnormal closure when the server closes and the client doesn't respond."""
1219+
close_code: Optional[WSCloseCode] = None
1220+
1221+
async def handler(request: web.Request) -> web.WebSocketResponse:
1222+
# Setting a short close timeout
1223+
ws = web.WebSocketResponse(timeout=0.1)
1224+
await ws.prepare(request)
1225+
await ws.close()
1226+
1227+
nonlocal close_code
1228+
assert ws.close_code is not None
1229+
close_code = WSCloseCode(ws.close_code)
1230+
1231+
return ws
1232+
1233+
app = web.Application()
1234+
app.router.add_route("GET", "/", handler)
1235+
client = await aiohttp_client(app)
1236+
async with client.ws_connect("/", autoclose=False):
1237+
await asyncio.sleep(0.2)
1238+
await client.server.close()
1239+
assert close_code == WSCloseCode.ABNORMAL_CLOSURE
1240+
1241+
1242+
async def test_normal_closure_while_client_sends_msg(
1243+
aiohttp_client: AiohttpClient,
1244+
) -> None:
1245+
"""Test abnormal closure when the server closes and the client doesn't respond."""
1246+
close_code: Optional[WSCloseCode] = None
1247+
got_close_code = asyncio.Event()
1248+
1249+
async def handler(request: web.Request) -> web.WebSocketResponse:
1250+
# Setting a short close timeout
1251+
ws = web.WebSocketResponse(timeout=0.2)
1252+
await ws.prepare(request)
1253+
await ws.close()
1254+
1255+
nonlocal close_code
1256+
assert ws.close_code is not None
1257+
close_code = WSCloseCode(ws.close_code)
1258+
got_close_code.set()
1259+
1260+
return ws
1261+
1262+
app = web.Application()
1263+
app.router.add_route("GET", "/", handler)
1264+
client = await aiohttp_client(app)
1265+
async with client.ws_connect("/", autoclose=False) as ws:
1266+
# send text and close message during server close timeout
1267+
await asyncio.sleep(0.1)
1268+
await ws.send_str("Hello")
1269+
await ws.close()
1270+
# wait for close code to be received by server
1271+
await asyncio.wait(
1272+
[
1273+
asyncio.create_task(asyncio.sleep(0.5)),
1274+
asyncio.create_task(got_close_code.wait()),
1275+
],
1276+
return_when=asyncio.FIRST_COMPLETED,
1277+
)
1278+
await client.server.close()
1279+
assert close_code == WSCloseCode.OK

0 commit comments

Comments
 (0)