Skip to content

fix(async): handle WebSocket binary frames and attachment collection race condition#506

Open
bneigher wants to merge 2 commits into1c3t3a:mainfrom
bneigher:fix/binary-attachment-handling
Open

fix(async): handle WebSocket binary frames and attachment collection race condition#506
bneigher wants to merge 2 commits into1c3t3a:mainfrom
bneigher:fix/binary-attachment-handling

Conversation

@bneigher
Copy link
Copy Markdown

@bneigher bneigher commented Feb 7, 2026

Summary

Fixes two bugs that cause the async Socket.IO client to crash when receiving binary events (e.g. Socket.IO v4 binary attachments sent as WebSocket binary frames):

1. Binary frame misidentification (InvalidUtf8 / InvalidPacketId)

WebSocket binary frames were prefixed with PacketId::Message (0x34 = '4'), causing the Engine.IO packet parser to treat raw binary attachment data as a UTF-8 text message. This produced InvalidUtf8 or InvalidPacketId errors on any binary event.

Fix: Binary frames are now prefixed with a 'B' (0x42) marker byte that the packet parser recognizes and maps to PacketId::MessageBinary, bypassing text/base64 decoding entirely.

Files:

  • engineio/src/asynchronous/async_transports/websocket_general.rs — use 'B' marker instead of PacketId::Message for binary WebSocket frames
  • engineio/src/packet.rs — recognize b'B' as PacketId::MessageBinary; handle raw binary path in TryFrom<Bytes>
  • engineio/src/asynchronous/async_socket.rs — short-circuit parse_payload for raw binary and single packets

2. Attachment collection race condition (EngineIO Error / lost packets)

handle_engineio_packet collected binary attachments via client.clone(), creating a second consumer on the same underlying stream. The outer iteration loop and the attachment loop then raced for incoming packets, causing binary frames to be consumed by the wrong loop and misinterpreted as Socket.IO text packets.

Fix: handle_engineio_packet is removed. Its logic is inlined directly into stream() using &mut client, so attachment packets are consumed sequentially from the same stream without contention.

Files:

  • socketio/src/asynchronous/socket.rs — inline attachment collection into stream(), remove handle_engineio_packet

Additional improvements

  • socketio/src/asynchronous/client/client.rshandle_binary_event now properly extracts the event name from quoted strings (e.g. "tts:audio") instead of stripping all quotes from the entire data field, which broke event routing for binary events with metadata.
  • socketio/src/error.rsIncompleteResponseFromEngineIo now includes the inner Engine.IO error in its display message ("EngineIO Error: {0}" instead of just "EngineIO Error").

Reproduction

Connect a rust_socketio async client (WebSocket transport) to a Socket.IO v4 server that emits binary events (e.g. socket.emit("audio", Buffer.from(...))). Without this fix, the client panics or errors with:

  • EngineIO Error (no details)
  • InvalidPacketId(66) (byte 0x42 from binary data interpreted as packet type)
  • InvalidUtf8 (binary data parsed as text)

Test plan

  • Verified against a Socket.IO v4 server (Go, zishang520/socket.io) emitting binary events with audio data
  • Verified text events, ack events, and disconnect/reconnect still work correctly
  • Existing CI test suite (binary event tests in socketio/src/asynchronous/client/client.rs)

Made with Cursor

…race condition

Two bugs were causing the async Socket.IO client to crash when receiving
binary events (e.g. Socket.IO v4 binary attachments over WebSocket):

1. **Binary frame misidentification**: WebSocket binary frames were prefixed
   with `PacketId::Message` (0x34 = '4'), causing the Engine.IO packet parser
   to treat raw binary data as a text message. This led to `InvalidUtf8` and
   `InvalidPacketId` errors. Fix: binary frames are now prefixed with a `'B'`
   (0x42) marker that is recognized by the packet parser and mapped to
   `PacketId::MessageBinary`, bypassing text decoding.

2. **Attachment collection race condition**: `handle_engineio_packet` collected
   binary attachments using `client.clone()`, which created a second consumer
   on the same underlying stream. This caused the outer iteration loop and the
   attachment loop to compete for packets, resulting in binary frames being
   consumed by the wrong loop and misinterpreted as Socket.IO text packets.
   Fix: attachment collection is now inlined into `stream()` using the same
   `&mut client`, so packets are consumed sequentially without contention.

Additional fixes:
- `parse_payload` in async_socket.rs short-circuits for raw binary packets
  and single packets (no record separator), avoiding unnecessary splitting.
- `handle_binary_event` properly extracts the event name from quoted strings
  in binary event packets instead of stripping all quotes from the data.
- `IncompleteResponseFromEngineIo` error now includes the inner error message.

Co-authored-by: Cursor <cursoragent@cursor.com>
@bneigher bneigher force-pushed the fix/binary-attachment-handling branch from f736325 to 22d7c2a Compare February 7, 2026 05:23
- Export RAW_BINARY_MARKER (pub) so async_socket can import it
- Use raw binary marker in packet parsing and websocket_general with comments
- Add [lints.rust] unexpected_cfgs allow for tarpaulin in engineio + socketio

Made-with: Cursor
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

Successfully merging this pull request may close these issues.

1 participant