Skip to content

Add WebSocket client module (WSClient)#23

Closed
carpentry-agent[bot] wants to merge 1 commit into
mainfrom
claude/ws-client
Closed

Add WebSocket client module (WSClient)#23
carpentry-agent[bot] wants to merge 1 commit into
mainfrom
claude/ws-client

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

Adds a WSClient module for outbound WebSocket connections with full RFC 6455 client-side masking. The web framework already had comprehensive WS server support — this adds the client side, reusing the existing frame codec, SHA-1, and Base64 implementations.

New public API

  • WSClient.connect addr port path — TCP connect + HTTP upgrade handshake with Sec-WebSocket-Accept validation
  • WSClient.connect-with-protocols addr port path protocols — connect with subprotocol negotiation
  • WSClient.send &client msg — send masked text frame
  • WSClient.send-binary &client &data — send masked binary frame
  • WSClient.ping &client &payload — send masked ping frame
  • WSClient.recv &client — blocking receive with automatic control frame handling (ping→pong, close→close) and fragmentation reassembly
  • WSClient.close &client — send close frame and shut down socket
  • WSClient.encode-masked-frame opcode &payload — build custom masked frames

Implementation details

  • encode-masked-frame generates a random 4-byte mask key per frame and XORs the payload (RFC 6455 §5.3)
  • do-handshake sends the HTTP upgrade request, validates the Sec-WebSocket-Accept header against the expected SHA-1 hash, and extracts any negotiated subprotocol
  • recv loops through buffered frames, handles control frames inline, and reassembles fragmented messages before returning a WSEvent
  • 15 new tests verify masked frame encoding round-trips through the existing decode-frame for text, binary, ping, close, empty payloads, 16-bit extended lengths, and multi-frame offset decoding

Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Build: CI passes on macOS. Local build fails on ARM/Linux due to a pre-existing TM.tm_zone const-qualification issue in the Carp compiler — confirmed the same error occurs on main, so this is not a regression from this PR.
Tests: CI passes. 15 new tests for masked frame encoding all pass.

Findings

Thorough code review of the WSClient module (~310 lines), handshake logic, frame encoding, recv loop, and tests.

Correctness — encode-masked-frame:

  • Mask generation uses Int.random-between 0 256 for each of 4 bytes — correct.
  • Frame header construction handles 7-bit (< 126), 16-bit (< 65536), and 64-bit length encodings correctly. The 64-bit path zeros the high 32 bits and encodes the low 32 bits, which is fine for any realistic payload size.
  • Mask bit (0x80) is correctly OR'd into the length byte.
  • XOR masking loop correctly indexes the mask key with Int.mod k 4.

Correctness — handshake:

  • HTTP upgrade request includes required headers (Host, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version).
  • Sec-WebSocket-Accept validation against the expected SHA-1 hash is correct, reusing the existing ws-accept-key function.
  • Subprotocol negotiation extracts Sec-WebSocket-Protocol from response headers.
  • Read timeout is set to 10s during handshake and reset to 0 after. Good.
  • Minor: the "101" check (String.contains-string? &resp "101") searches the entire response, not just the status line. In practice this is fine — "101" in a header value would be extremely unusual. Not worth blocking on.

Correctness — recv:

  • Control frame handling: ping → auto-pong (masked), pong → skip, close → echo close + return WSEvent.Close. All correct per RFC 6455.
  • Fragmentation reassembly: tracks frag-opcode for first fragment, appends continuation payloads, returns assembled message on final FIN frame. Correct.
  • compact-buf is called after processing to shift unprocessed bytes to the front. The double-copy could be optimized but is functionally correct.
  • On read error or connection close (n <= 0), returns appropriate error. Good.

Limitations (not bugs):

  • No wss:// (TLS WebSocket) support — only plain ws://. Reasonable scope boundary; TLS support would require TlsStream integration.
  • close sends a close frame but doesn't wait for the server's close response. This is common in client implementations and acceptable.
  • Tests cover frame encoding exhaustively (15 tests) but don't test connect/recv/send against a real server. Integration testing would require a WS server in the test harness — reasonable to defer.

Style:

  • The diff includes significant whitespace-only reformatting of existing code (indentation changes in web-try-ws-upgrade, handle-ws-upgrade, etc.). These are carp-fmt changes and harmless, but they add noise. Worth noting in case the formatter was run on the full file rather than just the changed sections.
  • CHANGELOG is properly updated.

Verdict: merge

Solid RFC 6455 client implementation with good test coverage of the frame codec. Reuses existing infrastructure (SHA-1, Base64, decode-frame) effectively. The limitations (no TLS, unilateral close) are reasonable scope boundaries, not bugs. Ready to merge.

@hellerve hellerve left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a web server framework, what do we need the client for?

@carpentry-agent

Copy link
Copy Markdown
Contributor Author

You're right — a WebSocket client doesn't belong in a server framework. Closing. If a client is wanted, it would fit better as a separate ws-client package.

@carpentry-agent carpentry-agent Bot closed this Jun 3, 2026
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