You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
WebSocket regression in 3.14.2: compressed data rejected after initial PONG #13274
aiohttp 3.14.2/3.14.3 rejects a valid compressed WebSocket data frame when the first inbound frame is a control frame.
The reproducible sequence is:
An aiohttp server sends PING.
The client replies with PONG.
The client sends a compressed TEXT frame using the negotiated permessage-deflate extension.
The aiohttp server closes with WebSocket protocol error 1002:
Received frame with non-zero reserved bits
The same sequence works correctly with aiohttp 3.14.1.
This appears to be a regression introduced by PR #12988 ("keep websocket compression state across interleaved control frames"). PONG followed by compressed application data is a
valid WebSocket sequence; control frames may be interleaved with data frames under RFC 6455 §5.4.
To Reproduce
import asyncio
import aiohttp
from aiohttp import WSMsgType, web
async def main() -> None:
pong_received = asyncio.Event()
server_result = asyncio.get_running_loop().create_future()
async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
# Disable server autoping so the reproducer can observe the PONG.
# Compression remains enabled and is negotiated with the client.
ws = web.WebSocketResponse(autoping=False, compress=True)
await ws.prepare(request)
await ws.ping()
pong = await ws.receive()
assert pong.type is WSMsgType.PONG
pong_received.set()
message = await ws.receive()
server_result.set_result((message.type, message.data, ws.exception()))
await ws.close()
return ws
app = web.Application()
app.router.add_get("/", websocket_handler)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", 0)
await site.start()
try:
async with aiohttp.ClientSession() as session:
async with session.ws_connect(
f"http://127.0.0.1:{site.port}/",
compress=15,
) as ws:
# receive() processes the server PING, automatically sends
# PONG, and then waits for the server's close frame.
receive_task = asyncio.create_task(ws.receive())
await pong_received.wait()
await ws.send_str("hello")
message_type, data, exception = await server_result
print(f"aiohttp {aiohttp.__version__}")
print(f"server message type: {message_type.name}")
print(f"server message data: {data!r}")
print(f"server exception: {exception!r}")
await receive_task
finally:
await runner.cleanup()
asyncio.run(main())
``` Run with:
$ python -m pip install aiohttp==3.14.3
$ python repro.py
### Expected behavior
The server should accept and decompress the TEXT message after receiving PONG.
Expected output, as produced by aiohttp 3.14.1:
aiohttp 3.14.1
server message type: TEXT
server message data: 'hello'
server exception: None
### Logs/tracebacks
```python-traceback
aiohttp 3.14.3
server message type: ERROR
server message data: WebSocketError(<WSCloseCode.PROTOCOL_ERROR: 1002>, 'Received frame with non-zero reserved bits')
server exception: WebSocketError(<WSCloseCode.PROTOCOL_ERROR: 1002>, 'Received frame with non-zero reserved bits')
Python Version
$ python --version Python 3.14.1
aiohttp Version
$ python -m pip show aiohttpVersion: 3.14.3
multidict Version
$ python -m pip show multidictVersion: 6.6.4
propcache Version
$ python -m pip show propcacheVersion: 0.3.2
yarl Version
$ python -m pip show yarlVersion: 1.20.1
OS
Ubuntu 26.04 LTS, Linux x86_64, glibc 2.43
Related component
Server
Additional context
Suspected cause:
PR #12988 changed _frame_fin so that only data frames update it, preserving the compression state when a control frame occurs inside a fragmented message.
However, compression-state selection still occurs before that condition. If PONG is the first inbound frame:
_compressed is set to COMPRESSED_FALSE from the control frame.
_frame_fin remains false because the frame is a control frame.
The next compressed data frame has RSV1 set.
Since _frame_fin is false and _compressed is no longer COMPRESSED_NOT_SET, the parser rejects RSV1 with protocol error 1002.
The regression is also reproducible with a Chromium browser connecting to an aiohttp server. Pinning aiohttp to 3.14.1 resolves it.
Submitted PR #13277 with the fix. The root cause is that control frames (PONG) were resetting self._compressed to COMPRESSED_FALSE, causing the next compressed data frame to be rejected. The fix wraps the compression state update in an opcode <= 0x7 check so only data frames (TEXT, BINARY, CONTINUATION) can modify the compression state.
Activity
waterWang commentedon Jul 28, 2026
Submitted PR #13277 with the fix. The root cause is that control frames (PONG) were resetting
self._compressedtoCOMPRESSED_FALSE, causing the next compressed data frame to be rejected. The fix wraps the compression state update in anopcode <= 0x7check so only data frames (TEXT, BINARY, CONTINUATION) can modify the compression state.Disable compression for WebSocket response to fix aio-libs/aiohttp#13274
fix(ws): stop "non-zero reserved bits" 1002s; idle close no longer au…
fix(ws): stop the aiohttp 1002 "non-zero reserved bits" kill; end con…
fix(ws): local-mode and ws-test sockets decline permessage-deflate (r…
test(conformance): reproduce PONG-then-data via raw frames instead of…
Fix #328: make test_first_data_frame_after_heartbeat_pong_is_accepted…