Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.http.websocket

import std.http.websocket speaks the WebSocket protocol of RFC 6455: the opening handshake, as a std.http.server exchange that switches protocols or as a client, then framed text and binary messages, ping and pong, and the closing handshake. std.http.wss adds wss clients through std.tls. Both modules are checked Plenty in stdlib/std/http/ and add no runtime primitives; the handshake’s SHA-1 comes from std.crypto. The tutorial lesson Talk over WebSockets shows a complete program.

import std.time
import std.bytes
import std.http.server
import std.http.websocket
import std.net
from std.http.server import Exchange
from std.http.websocket import Message, WebSocketError
from std.net import TcpStream
from std.sync import CancellationToken
from std.time import Deadline

type ChatError = WebSocketError | std.bytes.BytesError | RangeError | AllocError

def soon() -> Result[Deadline, RangeError]:
    Ok(std.time.deadline_after(&std.time.monotonic_now(), &std.time.seconds(5u32))?)

pub enum Route:
    Chat

pub class Shout:
    pub def handle(self, route: &Route, exchange: &mut Exchange[TcpStream]) -> Result[(), ChatError]:
        wait_cancel = exchange.cancel_token()
        mut socket = std.http.websocket.accept(exchange, &std.http.websocket.config())?
        mut message = std.bytes.empty()?
        while True:
            message.clear()
            match socket.receive(&mut message, &soon()?, &wait_cancel)?:
                case Message.Close(_):
                    return Ok(())
                case _:
                    socket.send_text(&message.to_text()?.upper()?, &soon()?, &wait_cancel)?
        Ok(())

def chat(port: u16, stop: CancellationToken) -> Result[(), ChatError | IoError]:
    with stop.cancel_on_exit():
        never = CancellationToken()?
        url = f"ws://127.0.0.1:{port}/chat"?
        mut socket = std.http.websocket.connect(&url, &std.http.websocket.config(), &soon()?, &never)?
        socket.send_text("hello", &soon()?, &never)?
        mut reply = std.bytes.empty()?
        print(socket.receive(&mut reply, &soon()?, &never)?, reply.to_text()?)?
        socket.close(1000, "bye", &soon()?, &never)?
    Ok(())

def main() -> Result[(), SpawnError[()] | std.http.server.ServerError | std.net.NetError | ChatError | IoError]:
    mut routes = std.http.server.router[Route]()?
    routes.add("GET", "/chat", Route.Chat)?
    listener = std.net.listen(&std.net.loopback_v4(0u16), 16)?
    port = listener.local_address()?.port
    stop = CancellationToken()?
    with spawn(chat, port, stop.share()).drop_job()? as task:
        drop(std.http.server.serve(listener, &routes, &Shout(), &std.http.server.config(), &stop)?)
        task.join()?
    Ok(())
std.http.websocket.Message.Text HELLO

Connections

WebSocket[T] is one open connection over a stream T that reads and writes like a server Transport: TcpStream, or TlsTransport for TLS.

ItemMeaning
socket.receive(&mut out, &deadline, &wait_cancel)Result[Message, WebSocketError]: append the next whole message to out
socket.send(kind, data: &[u8], &deadline, &wait_cancel)Result[(), WebSocketError]: send data as one Kind.Text or Kind.Binary message
socket.send_text(&text, &deadline, &wait_cancel)send of text, copied into a buffer first
socket.ping(data: &[u8], &deadline, &wait_cancel)Send a ping with at most 125 bytes of payload
socket.close(code, &reason, &deadline, &wait_cancel)Run the closing handshake, then shut the stream down
socket.is_open()Whether messages can still be sent
socket.subprotocolOption[str]: the subprotocol both sides agreed on

Message is Text, Binary, or Close(code). Text has been checked as UTF-8. A message sent in several frames is joined before receive returns it. Pings are answered with a pong and pongs are skipped while receiving. When the peer closes, receive answers with the same code and returns Close with the peer’s code, or 1005 when it gave none, and its reason appended to out; the connection is then closed.

send splits a message longer than max_frame into several frames. A Text message must be UTF-8 (InvalidText). Client frames are masked with a fresh key from std.secrets for each frame, as RFC 6455 requires; server frames are not.

close accepts the codes a peer may send: 1000 to 1003, 1007 to 1014, and 3000 to 4999, with a reason of at most 123 bytes (OutOfRange otherwise). It sends the close, skips any messages still arriving until the peer’s close, and shuts the stream down. Closing a connection whose closing handshake already finished returns Ok.

On an error, out keeps only what it held before the call. Every error except Stopping and OutOfRange closes the connection; later calls fail with Closed. A protocol fault first sends a close: 1002 for Protocol, 1009 for TooLarge and 1007 for InvalidText.

Each call has its own deadline. A connection holds one 4 KiB input buffer, grown only to hold input that arrived with the handshake, and one 4 KiB output buffer; a sent message is copied through the output buffer in pieces, so send allocates nothing for any message size.

Configuration

config() returns the defaults.

FieldDefaultMeaning
max_frame65536Largest frame payload received, and the size send splits messages into
max_message1048576Largest message receive joins
subprotocolNothingThe subprotocol a client offers, or a server accepts when the client offers it

Both maxima are Positive[i64], so a zero or negative maximum is a type error. They bound payload bytes, not the prefix already held by the caller’s buffer. An unrepresentable combined buffer extent is Allocation(AllocError.CapacityOverflow), not TooLarge; as with other receive errors, the original prefix is retained and the connection is closed. Frame splitting and mask positions stay within their source ranges even when the configured maximum is i64.MAX.

Servers

accept(exchange: &mut Exchange[T], config: &Config) returns Result[WebSocket[T], WebSocketError]. It checks that the request is a handshake: GET over HTTP/1.1 with no body, Upgrade: websocket, Connection: upgrade, Sec-WebSocket-Version: 13 and one Sec-WebSocket-Key of 16 bytes in base64. Field names and the Upgrade and Connection tokens are compared without case. A request that fails these checks is answered and accept returns Refused(status):

RequestAnswer
Missing websocket in Upgrade, or another version426 Upgrade Required with Upgrade: websocket and Sec-WebSocket-Version: 13
Any other fault400 Bad Request

Otherwise it answers 101 Switching Protocols with the accept value, and the configured subprotocol when the client listed it, and takes the connection with switch_protocols. The server no longer counts or closes the connection; dropping the WebSocket closes it.

The WebSocket keeps the server’s shutdown tokens. A receive waiting for the next message ends with Stopping when shutdown begins, and the connection stays open, so the handler can close with 1001. A wait in the middle of a message, or in send or close, ends with Net(Cancelled) when the drain deadline passes.

The handler keeps its worker for as long as the WebSocket is open, so each open connection counts against workers. Route a WebSocket to a handler that returns when the connection closes.

Clients

ItemMeaning
connect(url: &str, &config, &deadline, &cancel)Result[WebSocket[TcpStream], WebSocketError]: dial and handshake a ws URL
target(url: &str)Result[Target, WebSocketError]: parse a ws or wss URL
dial(&target, &deadline, &cancel)Result[TcpStream, WebSocketError]: a TCP connection to the target
handshake(stream, &target, &config, &deadline, &cancel)Result[WebSocket[T], WebSocketError]: the client handshake over a connected stream
Target.secure, host, portwss or not; the host, lowercase without IPv6 brackets; the port, 80 or 443 by default

target rejects URLs with another scheme (UnsupportedScheme), with credentials or a fragment, without a host, or with port 0 (InvalidUrl). The request target is the URL’s path, / when empty, and its query.

The handshake sends a random 16-byte key and checks the server’s answer: a status other than 101 is Rejected(status), and a 101 without the Upgrade and Connection tokens, with the wrong accept value, with extensions, or with a subprotocol the client did not offer is InvalidHandshake. The deadline bounds the handshake; later calls take their own. cancel ends every later wait with Net(Cancelled).

connect returns UnsupportedScheme for wss URLs: use std.http.wss.

WSS

import std.http.wss runs the client handshake over TLS.

ItemMeaning
connect(url: &str, &tls: ClientConfig, &config, &deadline, &cancel)Result[WebSocket[TlsTransport], WebSocketError] for a wss URL

The server’s certificate is verified against tls’s roots with the URL’s host as the name, as for HTTPS. A TLS failure is Connect(ClientError.Tls(...)). A ws URL is UnsupportedScheme. The module is separate so that a program using only ws does not link TLS. On the server side, accept takes exchanges from std.http.tls.serve unchanged.

Every message operation accepts a caller cancellation token after its deadline. Pre-cancellation returns Net(Cancelled) before changing the connection. Cancellation while receiving or writing closes the connection, since a partial frame cannot safely be resumed as a different operation. The retained server shutdown tokens keep their existing behavior. Native waits keep their shutdown wakeups and check the caller token between slices of at most 10 ms; scheduling and lock contention can delay observation beyond that interval.

Errors

WebSocketError has these cases:

CaseMeaning
Net(NetError)The stream failed, timed out, was cancelled, or ended early
Allocation(AllocError)A buffer could not grow
Http(HttpError)The handshake’s HTTP head could not be read or written
Connect(ClientError)The client could not connect, including TLS failures
Entropy(StreamError)No random bytes for a key or mask
InvalidConfig, InvalidUrl, UnsupportedSchemeSee above
OutOfRangeA ping payload, close code or reason out of bounds
Refused(status)The server refused a request that was not a handshake
Rejected(status)The server answered the client with another status
InvalidHandshakeA 101 answer that does not complete the handshake
ProtocolThe peer broke the framing rules
TooLargeA frame or message over the configured limits
InvalidTextText that is not UTF-8
ClosedThe connection has closed
StoppingServer shutdown began while waiting for a message

Limits

  • No extensions: permessage-deflate is neither offered nor accepted.
  • No proxy for clients; connect dials the URL’s host directly.
  • One caller at a time: receive and the sending methods all borrow the connection exclusively, so one thread cannot wait for a message while another sends.
  • A server WebSocket holds a worker until its handler returns.
  • receive places a control frame’s payload in out while handling it, so out briefly grows by up to 125 bytes.
  • A close answer echoes the peer’s code without its reason.
  • send_text copies its text; send sends bytes the caller already holds.