Open
@ThomasFarstrike

Description

On ESP32, after receiving 5-10 full websocket frames, this read returns only a partial frame:

payload = await self.reader.read(length)

This is to be expected, as the read(length) call in MicroPython's asyncio.Stream is not guaranteed to return the full length bytes in a single call, especially for large payloads, due to non-blocking I/O or buffer constraints.

I didn't observe this issue on unix/desktop MicroPython, only on the ESP32.

The proper way is to re-do the read() for the remaining length until the entire frame has been received.

The below fixes that, as well as adding some error handling.

--- old/aiohttp_ws.py  2025-05-20 14:06:16.111521205 +0200
+++ aiohttp/aiohttp_ws.py       2025-05-20 14:16:28.985286423 +0200
@@ -197,13 +199,31 @@
             return opcode, payload
         fin, opcode, has_mask, length = self._parse_frame_header(header)
         if length == 126:  # Magic number, length header is 2 bytes
-            (length,) = struct.unpack("!H", await self.reader.read(2))
+            length_data = await self.reader.read(2)
+            if len(length_data) != 2:
+                print("WARNING: aiohttp_ws.py failed to read 2-byte length, closing")
+                return self.CLOSE, b""
+            (length,) = struct.unpack("!H", length_data)
         elif length == 127:  # Magic number, length header is 8 bytes
-            (length,) = struct.unpack("!Q", await self.reader.read(8))
-
+            length_data = await self.reader.read(8)
+            if len(length_data) != 8:
+                print("WARNING: aiohttp_ws.py failed to read 8-byte length, closing")
+                return self.CLOSE, b""
+            (length,) = struct.unpack("!Q", length_data)
         if has_mask:  # pragma: no cover
             mask = await self.reader.read(4)
-        payload = await self.reader.read(length)
+            if len(mask) != 4:
+                print("WARNING: aiohttp_ws.py failed to read mask, closing")
+                return self.CLOSE, b""
+        payload = b""
+        remaining_length = length
+        while remaining_length > 0:
+            chunk = await self.reader.read(remaining_length)
+            if not chunk:  # Connection closed or error
+                print(f"WARNING: aiohttp_ws.py connection closed while reading payload, got {len(payload)}/{length} bytes, closing")
+                return self.CLOSE, b""
+            payload += chunk
+            remaining_length -= len(chunk)
         if has_mask:  # pragma: no cover
             payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
         return opcode, payload