Changelog
View SourceAll notable changes to this project are documented here. Format loosely follows Keep a Changelog; versions follow Semantic Versioning.
[0.5.0] - 2026-08-10
Fixed
ws_client:connect/2no longer loses a frame the server coalesced into the same TCP segment as its 101 response. The bytesread_response/4returned past the end of the handshake were discarded, so a server that greets the instant the upgrade completes left the connection silent. Reported and fixed by @longlene (#3).
Added
initial_datastart option, onws_session:start/1andws:accept/6: bytes the embedder read past the end of the handshake. The session replays them through the parser before it arms the transport, so they stay ahead of whatever the peer sends next. Use it instead of posting a synthetic transport message to the session, which leaves the order to chance.
Changed
ws_h1_tcp_serverhands the bytes it read past the request headers to the session throughinitial_datarather than forging a{tcp, Handle, Rest}message afterws:accept/6had already armed the socket. The old path also only coveredws_transport_gen_tcpandws_transport_ssl; any other transport hit acase_clause.ws_h2_upgrade(andws_h3_upgradethrough it) now handlesSec-WebSocket-Versionthe way RFC 8441 section 5 requires:validate_request/1,2rejects an extended CONNECT whose version is absent or not13with{error, {unsupported_version, V}}, matchingws_h1_upgrade, and surfacesversioninrequest_info();client_request/4,5sends the header. Servers that were accepting version-less CONNECT streams will now reject them.
0.4.0 - 2026-08-07
Added
- permessage-deflate (RFC 7692) is now wired end to end. The frame parser
accepts RSV1 on the first frame of a data message when the
compressparser option is on and delivers such messages as{compressed, text | binary, Payload};ws_frame:encode_compressed/2emits RSV1 frames. The session takes adeflateoption (the negotiated parameters), inflates inbound messages bounded bymax_message(a deflate bomb closes with 1009), validates text UTF-8 after inflating, and compresses outbound data frames with per-direction context takeover.ws_deflate:negotiate/2andws_deflate:parse_offer/1negotiate straight from the rawSec-WebSocket-Extensionselements the upgrade validators deliver. ws_client:connect/2acceptscompress => trueto offer permessage-deflate and run the session compressed when the server agrees.ws_frame:valid_utf8/1: whole-payload UTF-8 check for consumers validating inflated text.
Changed
- The peer's close frame now reaches the handler:
terminate/2receives{remote, Code, Reason}when the peer's close carried a status code, andremotefor a bare close. Other shutdown reasons are passed through unchanged.
0.3.0 - 2026-05-29
Changed
- Ported to Erlang/OTP 29. Old-style
catch Exprcleanup calls in the test suites and examples are rewritten astry ... catch _:_ -> ok endto satisfy OTP 29'swarn_deprecated_catch. The library sources were already free of old-style catch. - CI now tests OTP 28 and 29 (dropped 26 and 27) and uses rebar3 3.27.
0.2.0 - 2026-05-21
Security
ws_frameUTF-8 validation no longer crashes on a non-continuation byte in the middle of a multi-byte sequence (e.g.0xC2 0x41). Such input is now reported asinvalid_utf8so the session closes with code 1007 instead of the process crashing withbadarg.ws_sessionnow enforces anidle_timeout(default 60000 ms) on open connections and aclose_timeout(default 5000 ms) while awaiting the peer's close echo. A stalled peer can no longer hold a session and socket open indefinitely. Both acceptinfinityto disable and are configurable viaws:accept/6,ws:connect/2, andws_h1_tcp_server:start_link/1. Note: long-lived idle connections now require app-level pings oridle_timeout => infinity.
Fixes
ws_client:connect/2returns{error, {invalid_port, _}}for a non-numeric URL port instead of crashing, and accepts IPv6 literal hosts such asws://[::1]:8080/.ws_frame:encode/2caps an over-long close reason (> 123 bytes) on a UTF-8 codepoint boundary instead of crashing.
0.1.1 — 2026-04-19
Security
ws_h1_upgrade:validate_request/1,2no longer crashes on a malformedSec-WebSocket-Keyheader; invalid base64 now surfaces as{error, bad_sec_websocket_key}.ws_deflate:inflate/3,4caps inflated output (default 64 MiB, configurable) and returns{error, {inflate_too_big, _}}past the bound — defuses the classic permessage-deflate bomb.ws_client:connect/2caps pre-upgrade byte accumulation via a newmax_handshake_sizeoption (default 64 KiB). Exceeding it returns{error, handshake_response_too_big}.ws_h1_tcp_server:start_link/1gains the samemax_handshake_sizeoption (default 64 KiB). Over-limit returns{error, handshake_too_big}and the socket is closed.
Tests
- New
ws_security_SUITEwith 11 targeted cases covering each of the above plus state-machine invariants (send during closing, handler init stop, ordered multi-frame send).
Documentation
docs/embedding.md— new "Hardening" section summarising the embedder-side defences: handshake caps, timeouts, concurrent connections, origin enforcement, deflate bombs, TLS defaults, close-code handling.docs/errors.md— new "Size-limit errors" table.
Housekeeping
- Apache-2.0 copyright header added to every
.erlfile.
0.1.0 — 2026-04-19
Initial release.
Frame codec
- RFC 6455 encode / decode for all opcodes (text, binary, ping, pong, close, continuation).
- Masking / unmasking (32-bit XOR), symmetric and streamable.
- Fragmentation reassembly across continuation frames, with an open fragment state machine.
- UTF-8 validation for text payloads and close reasons via an inlined Hoehrmann DFA, streaming-safe across fragment boundaries.
- Close-code validation per RFC 6455 §7.4 (
ws_close). Reserved codes 1004 / 1005 / 1006 / 1015 rejected on the wire; 3000–4999 accepted for registration / application use. - Size guards:
max_frameandmax_messageoptions on the parser, enforced during single-frame decode and fragment accumulation.
Handshake helpers
ws_h1_upgrade— server-side validation ofUpgrade,Connection,Sec-WebSocket-Key,Sec-WebSocket-Version;Sec-WebSocket-Acceptgeneration; subprotocol negotiation; client-side key generation, request build, 101 response validation.ws_h2_upgrade— RFC 8441 extended CONNECT pseudo-header validation (:method=CONNECT,:protocol=websocket,:scheme,:authority,:path); server response builder; client request builder that refuses to issue a CONNECT when the peer has not advertisedSETTINGS_ENABLE_CONNECT_PROTOCOL = 1.ws_h3_upgrade— RFC 9220 mirror of the H2 helpers.
Session
ws_session—gen_statemdriving the session. Statesready_wait -> open -> closing. Auto-responds to peer pings with matching pongs; surfaces ping / pong to the handler for custom behaviour.- Orderly close handshake: on receiving a close frame, echoes a close back (only once) before exiting normally. Peer-initiated protocol violations trigger a close frame with the matching code (1002 protocol error / 1007 invalid UTF-8 / 1009 message too big) followed by socket shutdown.
ws_handlerbehaviour:init/2,handle_in/2,handle_info/2,terminate/2. All callbacks canreplywith outbound frames.
Transport layer
ws_transportbehaviour —send/2,activate/1,close/1,controlling_process/2,classify/2,recv/2(optional, used by the client during the HTTP/1.1 handshake),peername/1(optional).ws_transport_gen_tcp— referencegen_tcptransport used by tests.ws_transport_ssl— OTPssltransport used bywss://client connections.
Reference server
ws_h1_tcp_server— a minimalgen_tcpacceptor + HTTP/1.1 upgrade driver built on top of the library helpers. Supports plain TCP and TLS via thetlsoption. Doubles as a runnable example and the server-side driver ofws_examples_SUITE.
Client
ws_client:connect/2forws:///wss://URLs. Builds the RFC 6455 upgrade request, validates the 101 response (includingSec-WebSocket-Acceptcross-check), starts aws_sessionin client mode with the user handler.- Automatic subprotocol, extension, origin, and extra-header handling on the request side.
Extensions
ws_deflate— RFC 7692 permessage-deflate.- Server-side negotiation of client offers with configurable takeover / window-bits policy; ignore-on-disagreement semantics.
- Client-side offer builder and server-response parser.
- Inflate / deflate primitives with takeover management.
Examples
examples/echo_server.erl— RFC 6455 echo server backed byws_h1_tcp_server.examples/echo_client.erl— synchronous send-and-wait client, usesws:connect/2.examples/chat_server.erl— broadcast chat server usingpgfor fan-out between sessions.
Tests
- 142 EUnit tests across codec, close codes, handshake helpers, deflate.
- 4 PropEr properties: mask involution, round-trip in both directions, chunked-delivery preserves messages.
- 25 Common Test cases total:
- 10 in
ws_session_SUITE— text / binary / large-payload echo, ping/pong, fragmentation, orderly-close, server-initiated close, bad-UTF-8 rejection, oversize-frame rejection, handler info fan-out. - 3 in
ws_client_SUITE— client↔server round-trip. - 12 in
ws_examples_SUITE— end-to-end coverage of theexamples/modules plus subprotocol negotiation, 20 concurrent clients, TLSwss://round-trip, 512 KiB payload, fragmented text delivered raw.
- 10 in
- Autobahn compliance suite (
WS_RUN_AUTOBAHN=1) — 300 cases across sections 1–9. All OK / NON-STRICT / INFORMATIONAL; zero failures.
Documentation
README.md— quickstart with server and client examples, module map, embedder integration notes.docs/guide.md— tutorial (mental model, handler, server, client, sending frames, closing, subprotocols, TLS, limits). Every code snippet is mechanically verified byws_docs_snippets_SUITE.docs/embedding.md— HTTP/1.1, RFC 8441 (HTTP/2), and RFC 9220 (HTTP/3) integration patterns plus instructions for writing a customws_transport.docs/errors.md— every failure mode the library surfaces, with close codes and remediation.docs/features.md— RFC coverage, hardening list, scope boundaries, full module map.LICENSE— Apache-2.0.