Skip to content

Fix attached for aiohttp's aiohttp_ws.py partial read on ESP32 #1012

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

Open
ThomasFarstrike opened this issue May 20, 2025 · 0 comments
Open

Comments

@ThomasFarstrike
Copy link

ThomasFarstrike commented May 20, 2025

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 patch 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant