HTTP.WebSocket (http_web_socket v1.0.0-rc.4)

Copy Markdown View Source

Browser-like WebSocket client API for Elixir.

Events are delivered as messages to the owner process:

{HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Open{}}
{HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Message{}}
{HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Error{}}
{HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Close{}}

HTTP/1 is the default. Select http_version: :http2, :h2c, or :auto explicitly; http_version/1 reports the negotiated HTTP version, while protocol/1 continues to report the selected WebSocket subprotocol.

With delivery: :ack, message events include a delivery reference:

{HTTP.WebSocket, socket, %HTTP.WebSocket.Event.Message{}, ref}

Call acknowledge/2 after consuming each message. Open, Error, and Close retain their three-element event envelopes. Receive queues, pending sends, and fragmented messages have finite configurable limits. Established sessions default to idle_timeout: :infinity; opening and close deadlines are separate.

connect_address: {127, 0, 0, 1} (or an IPv6 tuple) pins this connection's literal dial address. The URL still supplies Host, HTTP/2 authority, SNI and certificate reference identity. DNS and alternate-address retries are disabled for a pinned dial; connection failures and upgrades are never replayed. Pins cannot be combined with Unix sockets or conflicting TLS identities. Protocol selection with http_version: :auto retains its explicit H2/H1 negotiation policy, with every dial using the same pin.

Open and Error events have handshake: %{status: status, headers: headers, truncated?: boolean} once a final response head has been parsed, or nil before that point. headers is an HTTP.Headers containing only retry-after and x-codex-websocket-max-unfragmented-message-bytes. Each is retained only when it occurs exactly once and its value is at most 1024 bytes; otherwise it is omitted and truncated? is true. Values are copied, so this metadata retains at most 2048 value bytes and no other response headers. Failed upgrades retain their original Error reason. This applies to HTTP/1 and HTTP/2.

Receive limits are independent: max_message_size bounds each reassembled message (16 MiB by default); max_frame_bytes bounds each frame payload (defaults to the larger of the message limit and 125). Declared lengths are checked before awaiting payload bytes, including continuations. A message may span many bounded frames. max_read_bytes bounds queued raw bytes plus the parser's incomplete frame and each incoming transport chunk, including bytes coalesced with the upgrade. Its default is the existing raw queue allowance (1 MiB plus the H2 profile's initial stream window) plus the larger of the message limit and 125, plus ten header bytes. It excludes reassembled fragments and delivered events, which have their own message and delivery bounds. It does not bound kernel or VM transport allocations. All three options require positive integers. Choose a read limit that accommodates a full frame header and payload, and any coalesced frames you intend to accept.

Limit failures emit Error reasons :frame_too_big, :message_too_big, or :read_buffer_too_big, followed by an abnormal Close with code 1009. An individually declared payload above the message limit keeps the existing :message_too_big reason; otherwise the frame bound is checked before the accumulated message bound. Read admission precedes parsing. These receive options do not fragment outgoing sends; send queues keep their existing bounds.

Telemetry URLs omit query, userinfo, authority, and fragment by default. Pass telemetry_url: nil to omit all URL-derived telemetry metadata, or a URI/string containing only caller-approved fields to replace the telemetry URL (for example, telemetry_url: "ws://127.0.0.1:8080" omits a private path). Invalid replacements reject construction; replacement credentials are always stripped. This option applies to every lifecycle/message event and never changes the transport URL or handshake.

HTTP/1 WebSocket proxy boundary

For frame forwarding, select mode: :proxy, http_version: :http1, delivery: :ack and automatic_pong: false. Proxy mode delivers %HTTP.WebSocket.Event.Frame{opcode: opcode, data: binary} for text, binary, ping and pong, with the same delivery reference/acknowledge/2 contract. binary_type does not alter proxy frame data. Close retains its lifecycle event and automatic reply. send_frame/2 and send_frame_ack/2 accept {opcode, binary} for those four opcodes. Automatic pong defaults to true; disabling it requires proxy mode. Proxy close/3 accepts nil with an empty reason, 1000-1014 except 1004/1005/1006, and 3000-4999. Reserved/non-wire codes, invalid UTF-8 and reasons over 123 bytes remain rejected.

Proxy HTTP/2, h2c and auto selection return {:error, :proxy_requires_http1}. Wait for successful completion of every send_frame_ack/2 reference before initiating Close: local close discards queued application frames in both HTTP versions. This provides ordered local write completion over HTTP/1; proxy mode supplies no HTTP/2 drain contract. Control sends share the bounded application send FIFO, including zero-length payloads. Control delivery uses the receive limits too; ACK capacity must accommodate at least 132 bytes.

Use this public client for the upstream half of a WebSocket proxy. Select http_version: :http1 and delivery: :ack; acknowledge each Message only after downstream consumption. Set finite opening_timeout, idle_timeout, write_timeout and close_timeout, and size/count limits for messages, receive queues and pending sends. send_ack/2 reports local transport completion for each admitted frame. send/2 reports admission only.

The client validates status 101, Upgrade/Connection tokens, exactly one matching Sec-WebSocket-Accept and the selected subprotocol. Bytes coalesced after the response head enter the frame parser. Redirects and uncertain operations are never replayed. Ping is answered with Pong automatically; Close is exchanged with the peer. This interface proxies WebSocket messages, not raw TCP bytes or exact frame fragmentation. Forward binary messages with array_buffer/1 and configure the upstream protocols explicitly.

WSS retains the selected TLS backend's CA, hostname and SNI verification; supply trusted ssl: [cacertfile: ...] for private CAs. Owner death closes the connection. close/1 signals cancellation; use completion/1 with HTTP.ClientCompletion.await/2 to confirm cleanup, including a cancelled opening. The receipt remains readable after the socket process exits. A Close event describes the handshake result; it does not prove peer application receipt of earlier writes. Terminal cancellation discards pending transport bytes, so a failed write may have reached the peer partly.

Plain Elixir binaries are sent as text frames. Use array_buffer/1 or HTTP.Blob for binary frames.

For wss connections, tls_backend: :ex_ssl (or the equivalent string in a map) selects the TLS provider. When omitted, the shared :http_core TLS backend setting defaults to :ex_ssl and is captured when the socket is created. Legacy OTP :ssl selections are rejected.

Summary

Functions

Acknowledges an ACK-mode message delivery reference.

Returns the durable cleanup receipt without contacting the connection.

Returns the negotiated HTTP version, or nil before establishment.

Admits a bounded send and returns its completion reference.

Admits an explicit text, binary, ping or pong frame in mode: :proxy.

Admits an explicit proxy frame and returns a local write completion reference.

Returns connection state and bounded delivery and send queue usage.

Types

handshake()

@type handshake() :: %{
  status: non_neg_integer(),
  headers: HTTP.Headers.t(),
  truncated?: boolean()
}

t()

@type t() :: %HTTP.WebSocket{
  completion: HTTP.ClientCompletion.t() | nil,
  pid: pid() | nil,
  ref: reference() | nil,
  url: String.t() | nil
}

Functions

acknowledge(socket, ref)

@spec acknowledge(t(), reference()) :: :ok | {:error, term()}

Acknowledges an ACK-mode message delivery reference.

array_buffer(data)

@spec array_buffer(binary()) ::
  HTTP.WebSocket.ArrayBuffer.t() | {:error, :invalid_array_buffer}

binary_type(socket)

@spec binary_type(t()) :: :blob | :array_buffer

buffered_amount(socket)

@spec buffered_amount(t()) :: non_neg_integer()

close(socket)

@spec close(t()) :: :ok | {:error, term()}

close(socket, code)

@spec close(t(), non_neg_integer()) :: :ok | {:error, term()}

close(socket, code, reason)

@spec close(t(), non_neg_integer() | nil, String.t()) :: :ok | {:error, term()}

closed()

@spec closed() :: 3

closing()

@spec closing() :: 2

completion(web_socket)

@spec completion(t()) :: HTTP.ClientCompletion.t() | nil

Returns the durable cleanup receipt without contacting the connection.

connecting()

@spec connecting() :: 0

extensions(socket)

@spec extensions(t()) :: String.t()

http_version(socket)

@spec http_version(t()) :: :http1 | :http2 | nil

Returns the negotiated HTTP version, or nil before establishment.

new(url, protocols \\ [], init \\ [])

@spec new(String.t() | URI.t(), String.t() | [String.t()], keyword() | map()) ::
  t() | {:error, term()}

open()

@spec open() :: 1

protocol(socket)

@spec protocol(t()) :: String.t()

ready_state(socket)

@spec ready_state(t()) :: 0 | 1 | 2 | 3

send(socket, data)

@spec send(t(), String.t() | HTTP.Blob.t() | HTTP.WebSocket.ArrayBuffer.t()) ::
  :ok | {:error, term()}

send_ack(socket, data)

@spec send_ack(t(), String.t() | HTTP.Blob.t() | HTTP.WebSocket.ArrayBuffer.t()) ::
  {:ok, reference()} | {:error, term()}

Admits a bounded send and returns its completion reference.

The owner receives {HTTP.WebSocket, socket, {:send_result, ref, result}}, where result is :ok only after the complete frame was accepted by the transport, or {:error, reason} on cancellation/failure. This confirms local write completion, not peer application receipt. Failed/uncertain writes are never replayed. Unlike send/2, a closed connection rejects admission. Queuing and writing share the finite write_timeout (default 5 seconds).

send_frame(socket, frame)

@spec send_frame(t(), {:text | :binary | :ping | :pong, binary()}) ::
  :ok | {:error, term()}

Admits an explicit text, binary, ping or pong frame in mode: :proxy.

Control payloads are limited to 125 bytes; text must be valid UTF-8. Frames share the bounded FIFO send queue and write deadline with send/2. This reports admission only; use send_frame_ack/2 before forwarding Close. Send Close using close/3, preserving the connection's close deadline.

send_frame_ack(socket, frame)

@spec send_frame_ack(t(), {:text | :binary | :ping | :pong, binary()}) ::
  {:ok, reference()} | {:error, term()}

Admits an explicit proxy frame and returns a local write completion reference.

The owner receives the same {:send_result, ref, result} event as send_ack/2. Wait for :ok for every admitted frame before calling close/3 to preserve prior writes. Completion does not prove peer application receipt.

set_binary_type(socket, binary_type)

@spec set_binary_type(t(), :blob | :array_buffer) :: :ok | {:error, term()}

status(socket)

@spec status(t()) :: map()

Returns connection state and bounded delivery and send queue usage.

url(web_socket)

@spec url(t()) :: String.t() | nil