#WebSocket API for MoonBit Async Library

This module provides RFC 6455 compilant WebSocket client & server support for moonbitlanga/async.

#Features

  • WebSocket server with support for integration into a normal HTTP server
  • WebSocket client with support for TLS and proxy

#Quick Start

#WebSocket server

// A simple WebSocket echo server that listen on path `/ws`,
// and reply clients with whatever they send.

///|
#cfg(any(target="native", target="wasm"))
async fn websocket_echo_server(addr : @socket.Addr) -> Unit {
let server = @http.Server(addr)
// WebSocket server are built on top of a HTTP server.
// This allows mixing WebSocket and HTTP in a single server.
server.run_forever((request, _, conn) => {
guard request.path is "/ws" else { conn.send_response(404, "NotFound") }
let ws = @websocket.from_http_server(request, conn)
defer ws.close()
for ;; {
// Receive a new message using `ws.recv()`
// WebSocket message can be very long,
// so `ws.recv()` does not read the full message into memory.
// Instead, it returns a `@websocket.Message` type,
// whose content can be read lazily via `@io.Reader`.
let msg = ws.recv()
// Reply the client with the message we just received.
// `msg.kind` is either `Text` (UTF-8 encoded text message)
// or `Binary` (plain binary message).
//
// Sending message can also be performed lazily.
// `.start_message(message_kind)` begins a new message,
// after which message content can be written lazily to the WebSocket tunnel.
// After all message content is written,
// `.end_message()` should be called to terminate the message.
// Fragmentation of message will be performed automatically.
ws..start_message(msg.kind)..write_reader(msg).end_message()
}
})
}

#WebSocket client

///|
#cfg(any(target="native", target="wasm"))
async test "WebSocket client example" {
@async.with_task_group(group => {
let port = 42080
let addr = @socket.Addr::parse("127.0.0.1:\{port}")
group.spawn_bg(no_wait=true, () => websocket_echo_server(addr))

// Create a new client via `@websocket.connect`.
// TLs encrypted WebSocket tunnel can be created via `wss://` URL.
let ws = @websocket.connect("ws://localhost:\{port}/ws")
defer ws.close()
// convenient helper for sending small text message
ws.send_text("abcd")
inspect(ws.recv().read_all().text(), content="abcd")
// gracefully terminate the WebSocket tunnel
ws.send_close()
})
}

#API Reference

#Conn

The type @websocket.Conn represents a WebSocket tunnel. It can be created in the following three ways:

  • @websocket.connect(url : String, headers?, proxy?): connect to url via HTTP, perform WebSocket handshake and establish a new tunnel. If the url starts with ws://, the tunnel will be unencrypted, and handshake is performed via plain HTTP. If the url starts with wss://, a TLS encrypted tunnel will be created, and handshake is performed via HTTPS. The optional argument headers can be used to specify addition headers in the handshake. The optional argument proxy can be used to specify a HTTP(s) proxy for the tunnel.
  • @websocket.from_http_client(client : @http.Client, path: String): similar to @websocket.connect, but create the WebSocket tunnel using an existing http client.
  • @websocket.from_http_server(request : @http.Request, conn : @http.ServerConnection): process a WebSocket handshake request request from a client, and upgrade the HTTP server connection conn to a WebSocket tunnel.

After successfully establishing a WebSocket tunnel, the following API can be used to do things with the tunnel:

  • recv() -> Message: receive a new message from the tunnel. The kind of the message can be retrieved via msg.kind. recv() will not actually receive the content of the message, the content of the message should be obtained by reading from the Message type.
  • send_text(text : StringView): send a new text message
  • send_binary(data : BytesView): send a new binary message
  • start_message(kind : MessageKind): start a new message of kind kind. The content of the new message can be written to the WebSocket tunnel via API in @io.Writer
  • end_message(): terminate current message created via start_message(), flush remaining content of message to the server
  • send_close(): gracefully terminate the WebSocket tunnel by sending a CLOSE frame. This method should only be used when no network or protocol error occurs.
  • close(): dispose a WebSocket tunnel, release related resource. The underlying HTTP connection will be closed as well.
  • impl @io.Writer for Conn: write content to the message currently being sent, created via .start_message()
  • ping(): actively send a WebSocket PING message and wait for corresponding PONG reply. .ping() can be called inside another message being sent. However, .ping() must be called when there is another task receiving message from the tunnel in parallel, otherwise the PONG reply can not be fetched, and .ping() will hang forever.

#Message

A single message received from a WebSocket tunnel.

  • field kind: the kind of the message
  • impl @io.Reader for Message: read content of the message. For example, content of the whole message can be obtained via .read_all()

#CloseCode

WebSocket close code.

///|
pub(all) enum CloseCode {
Normal // 1000
GoingAway // 1001
ProtocolError // 1002
UnsupportedData // 1003
Abnormal // 1006
InvalidFramePayload // 1007
PolicyViolation // 1008
MessageTooBig // 1009
MissingExtension // 1010
InternalError // 1011
Other(UInt16)
} derive(Debug, Eq)

#Error Types

#WebSocketError

///|
suberror WebSocketError {
// Connection was closed with specific code.
// with an optional message for close reason.
ConnectionClosed(CloseCode, String?)
InvalidHandshake(String) // Handshake failed with detailed reason
ProtocolError // Malformed frame
} derive(Debug)

WebSocketError

pub suberror WebSocketError {
ConnectionClosed(CloseCode, String?)
InvalidHandshake(String)
HandshakeRejected(String,
Response
)
ProtocolError(String)
} derive(
Debug
)

CloseCode

pub(all) enum CloseCode {
Normal
GoingAway
ProtocolError
UnsupportedData
Abnormal
InvalidFramePayload
PolicyViolation
MessageTooBig
MissingExtension
InternalError
Other(UInt16)
} derive(Eq,
Debug
)

WebSocket close status codes
impl Show for CloseCode

Conn

type Conn

A WebSocket connection
impl Writer for Conn

Conn::close

fn Conn::close(self : Conn) -> Unit

Close a WebSocket connection. The underlying HTTP connection will be closed as well.

Conn::connect

#as_free_fn
async fn Conn::connect(url : String, headers? : Map[
CaseInsensitiveString
, String], proxy? :
Client
) -> Conn

Connect to a WebSocket server via the given URL. The protocol of the URL must be either ws (for unencrypted WebSocket connection) or wss (for TLS encrypted WebSocket connection).

Extra headers during the WebSocket handshake can be specified via extra_headers. Some headers are reserved for the WebSocket protocol and must NOT be set in extra_headers:

  • those headers set by @http.Client, see @http.Client::new
  • Connection, Upgrade
  • Sec-WebSocket-Key, Sec-WebSocket,Version

If proxy is present, the websocket client will tunnel traffic through the proxy. See @http.Client::new for more details.

Example:
let ws = Client::connect("ws://example.com/endpoint")

Conn::end_message

async fn Conn::end_message(self : Conn) -> Unit

End the message currently being sent, flush all buffered data and tell the server the termination of current message.

end_message must be called after the start_message.

Conn::from_http_client

#as_free_fn
async fn Conn::from_http_client(conn :
Client
, path : StringView, extra_headers? : Map[
CaseInsensitiveString
, String]) -> Conn

Create a WebSocket tunnel from an existing HTTP client. The HTTP client must be in a clean state (i.e. not in the middle of a request). A WebSocket handshake will be sent to path via the HTTP client. If the handshake succeeds, a WebSocket tunnel will be established and returned.

The ownership of the HTTP client will be transferred to this function. So caller must not use the client anymore, nor close it.

Extra headers during the WebSocket handshake can be specified via extra_headers. Some headers are reserved for the WebSocket protocol and must NOT be set in extra_headers:

  • those headers set by @http.Client, see @http.Client::new
  • Connection, Upgrade
  • Sec-WebSocket-Key, Sec-WebSocket,Version

Conn::from_http_server

Handle a WebSocket handshake request and convert an existing HTTP server connection to a WebSocket tunnel.

  • request: the WebSocket handshake request
  • conn: the HTTP server connection

If the handshake succeeds, a new WebSocket tunnel will be created and returned.

The ownership of conn will be transferred to @websocket.ServerConnection::from_http, so the user must NOT use or close the HTTP server connection anymore.

Conn::ping

async fn Conn::ping(self : Conn, msg? : BytesView) -> Unit

Send a PING frame to the peer, and wait for PONG reply. If msg is provided, its content will become the body of the PING message. Otherwise, ping() automatically generate random bytes as message body.

The PONG reply may not come immediately. In particular, it may arrive after several other messages already on the wire. To avoid data loss and race condition, .ping() itself will not wait for the PONG reply directly. User must receive data from the same WebSocket connection somewhere else, in order to get the PONG reply of a PING request. Calling .ping() without another task running .recv() on parallel WILL RESULT IN DEAD LOCK.

If a PING request with the same message body is still waiting for reply, .ping() will fail immediately with error.

Conn::recv

async fn Conn::recv(self : Conn) -> Message

Conn::send_binary

async fn Conn::send_binary(self : Conn, data : BytesView) -> Unit

Convenient helper for sending a single binary message. To send large message lazily, see start_message.

Conn::send_close

async fn Conn::send_close(self : Conn, code? : CloseCode, reason? : String) -> Unit

Initiating the connection closing process of WebSocket. Note that send_close merely performs closing at WebSocket protocol level, so:

  • Conn::close should be called anyway, even if send_close is called
  • Conn::send_close should only be called when everything goes well. For example it should NOT be called when protocol error or network error occurs.

Conn::send_text

async fn Conn::send_text(self : Conn, text : StringView) -> Unit

Convenient helper for sending a single text message. To send large message lazily, see start_message.

Conn::start_message

fn Conn::start_message(self : Conn, kind : MessageKind) -> Unit

Start sending a new message to the server. The content of the message can be sent by using the self as a @io.Writer after calling start_message. end_message must be explicitly called to terminate the message.

Writing message content is buffered. To ensure immediate delivery of data, split them into multiple messages, and use end_message to ensure data is actually sent to the server.

start_message must NOT be called before the last message ends.

Conn::write

async fn Conn::write(self : Conn, data : &
Data
) -> Unit

Conn::write_once

async fn Conn::write_once(self : Conn, buf : Bytes, offset~ : Int, len~ : Int) -> Int

Conn::write_reader

async fn Conn::write_reader(self : Conn, reader : &
Reader
) -> Unit

Message

pub struct Message {
kind : MessageKind
// private fields
}

A message received from a WebSocket tunnel
impl Reader for Message

Message::drop

async fn Message::drop(self : Message, len : Int) -> Int

Message::read

async fn Message::read(self : Message, dst : FixedArray[Byte], offset? : Int, max_len? : Int) -> Int

Message::read_all

async fn Message::read_all(self : Message) -> &
Data

Message::read_exactly

async fn Message::read_exactly(self : Message, len : Int) -> Bytes

Message::read_some

async fn Message::read_some(self : Message, max_len? : Int) -> Bytes?

Message::read_until

async fn Message::read_until(self : Message, sep : StringView) -> String?

MessageKind

pub(all) enum MessageKind {
Binary
Text
} derive(
Debug
)

WebSocket message
impl Show for MessageKind

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io