Documentation channel: current development main at compiler checkpoint 1d7e15e. The latest tagged release is v0.0.3; pages identify APIs that are not yet released.

WebSockets

Current main, unreleased: this page documents compiler checkpoint 1d7e15e. The WebSocket APIs do not exist in v0.0.3 and must not be treated as part of that release contract.

Current main adds synchronous RFC 6455 server upgrades (P4), bounded frame/message handling (P5), and safe application-level composition with PTYs (P10) on the built-in HTTP server. It is a server API only: there is no outbound WebSocket client and no extension or compression implementation. Certification covers focused protocol, resource, plaintext, TLS, full-duplex, and WebSocket-to-PTY cases, not production soak or internet-scale long-lived connections.

Server example

function configure() -> void : (NetworkError, WebSocketError) {
    app := http_server();
    app.timeouts(30000, 30000, 5000, 5000);
    app.websocket_limits(1048576, 4194304);
    app.websocket("/echo", (http_request request, websocket socket) => {
        socket.accept();
        text := socket.read_text();
        socket.write_text(text ?? "");
        socket.close(1000, "done");
        return;
    });
}
function main() -> int {
    return 0;
}

Upgrade ownership (P4)

app.websocket(route, handler) registers a synchronous (http_request, websocket) -> void handler. It receives the normal route parameters, query values, cookies, headers, and request cancellation token plus a pending request-scoped socket. Inspect Origin, authorization, and offered protocols before acceptance; Strut does not supply an origin or authentication policy.

socket.accept() commits a 101 response without a subprotocol. socket.accept(protocol) selects and echoes exactly one protocol offered by the client. An invalid or unoffered selection raises NetworkError. Repeating the same acceptance after full commitment is harmless; changing it is an error. A failed or possibly partial 101 write is terminal and cannot fall back to an HTTP response.

Returning without acceptance rejects with 403. An exception before acceptance produces 500. An exception after acceptance attempts a 1011 close, then cleans up the transport.

Opening validation

An opening must be GET over HTTP/1.1 with a Connection token containing Upgrade, an Upgrade product list containing the bare websocket product, version 13, and a canonical padded Base64 key decoding to 16 bytes. Repeated opening fields, transfer coding, and nonzero content length are rejected. Unsupported versions receive 426 with Sec-WebSocket-Version: 13. Upgrades not claimed by a matched WebSocket route receive 501.

Offered subprotocols must be a nonempty comma-separated list of unique RFC tokens and remain visible in request.headers["sec-websocket-protocol"]. Syntactically valid extension offers are visible but always declined by omission from the 101 response.

Messages and frames (P5)

socket.read() returns a complete websocket_message?, or null after peer close. Its read-only kind is "text" or "binary"; only the matching nullable text or data field is populated. read_text() and read_bytes() return the matching nullable payload. On a kind mismatch they raise WebSocketError and retain that one message for a later compatible read rather than discarding it.

write_text(string) and write_bytes(bytes) send complete server messages. ping() or ping(bytes) sends up to the RFC control-frame limit. Incoming Ping is answered automatically and Pong is internal. Text and close reasons require valid UTF-8; binary is opaque.

close(), close(code), and close(code, reason) send one validated close frame and are idempotent once sent. The default code is 1000. Server code 1010 is rejected, other codes must be registered or application-valid, and the UTF-8 reason is limited to 123 bytes. Peer close is answered once and makes later reads return null.

Protocol enforcement

Client frames must be masked. RSV bits, reserved opcodes, non-minimal or high-bit 64-bit lengths, invalid fragmentation or control transitions, malformed close payloads, and invalid UTF-8 fail closed with WebSocketError. When transport is still usable, the server first attempts an appropriate 1002, 1007, or 1009 close. Disconnect, timeout, TLS failure, and interrupted transport remain NetworkError.

Limits and backpressure

app.websocket_limits(frame_bytes, message_bytes) may be called only while the server is stopped. Defaults are 1 MiB per data-frame payload and 4 MiB per reassembled message. The frame limit must be at least 125 bytes and no greater than the positive message limit. Control frames always retain their independent 125-byte maximum. Declared frame sizes and cumulative fragments are checked before oversized allocation; there are no other WebSocket size knobs.

Exactly one read may be active; another fails immediately with WebSocketError. Writes are serialized and synchronous against TCP/TLS backpressure without an unbounded frame queue. One read and one serialized write may progress concurrently. Plain TCP uses independent receive and send directions. After a TLS upgrade, one nonblocking owner thread is solely responsible for the SSL* while caller threads may wait independently on at most one bounded request per direction.

The TLS owner pins the exact buffer for an incomplete write retry and uses absolute request deadlines. An abandoned retry, deadline, TLS failure, or interruption is terminal for that connection. The ordinary frame parser still applies its read timeout per transport I/O operation rather than as an absolute whole-frame deadline, so a peer continuously drip-feeding bytes can retain one bounded server worker until shutdown or an application/network policy closes it.

PTY sessions (P10)

P10 adds no bridge API and no runtime dependency from WebSockets to PTYs or from PTYs to WebSockets. Application code owns two public-API pumps: validated binary WebSocket input and explicit text controls drive the PTY, while a joined task copies bounded terminal output chunks through socket.write_bytes. Pass request.cancellation to pty_spawn, close the PTY on every terminal path, and do not place an unbounded application queue between the synchronous WebSocket and PTY writes.

The generated certification fixture covers policy rejection before 101, coalesced upgrade input, plaintext and TLS, shell and ordinary executable launch, binary input/output, resize and lifecycle controls, final-output drain, slow peers, clean and abrupt disconnects, blocked-input cancellation during server shutdown, 1,000 sequential sessions, and 32 bounded concurrent sessions. Windows output remains ConPTY VT traffic rather than a byte-transparent POSIX stream.

Lifetime and shutdown

Socket copies share request-scoped state and become invalid when the handler returns or throws. Teardown stops new operations, interrupts transport I/O, drains already admitted accept/parser/writer operations, and only then releases worker-owned TCP/TLS state. Escaped copies fail safely rather than extending the route lifetime.

On normal return, explicit close plus return, or a post-accept handler error, the worker sends at most one normal, explicit, or 1011 Close and waits up to one second for peer Close when it can own the parser; Ping is still answered. If another reader owns the parser, graceful waiting is skipped and that reader is interrupted and drained. Server shutdown interrupts either wait immediately.

TLS and dependencies

Use listen_tls for wss; it has the same certificate, TLS 1.2 minimum, timeout, and no-mTLS limitations documented on the HTTP server page. Plaintext WebSockets use an internal handshake-only SHA-1 implementation and add no OpenSSL dependency. Plain HTTP programs that never reference WebSockets omit the handshake and frame/message runtime.

Not provided

Current main does not provide a WebSocket client, permessage-deflate or other extensions, HTTP/2 extended CONNECT, application heartbeats, automatic authentication/origin policy, session fan-out, broker semantics, or internet-facing deployment tooling.