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
@type handshake() :: %{ status: non_neg_integer(), headers: HTTP.Headers.t(), truncated?: boolean() }
@type t() :: %HTTP.WebSocket{ completion: HTTP.ClientCompletion.t() | nil, pid: pid() | nil, ref: reference() | nil, url: String.t() | nil }
Functions
Acknowledges an ACK-mode message delivery reference.
@spec array_buffer(binary()) :: HTTP.WebSocket.ArrayBuffer.t() | {:error, :invalid_array_buffer}
@spec binary_type(t()) :: :blob | :array_buffer
@spec buffered_amount(t()) :: non_neg_integer()
@spec close(t(), non_neg_integer()) :: :ok | {:error, term()}
@spec close(t(), non_neg_integer() | nil, String.t()) :: :ok | {:error, term()}
@spec closed() :: 3
@spec closing() :: 2
@spec completion(t()) :: HTTP.ClientCompletion.t() | nil
Returns the durable cleanup receipt without contacting the connection.
@spec connecting() :: 0
@spec http_version(t()) :: :http1 | :http2 | nil
Returns the negotiated HTTP version, or nil before establishment.
@spec open() :: 1
@spec ready_state(t()) :: 0 | 1 | 2 | 3
@spec send(t(), String.t() | HTTP.Blob.t() | HTTP.WebSocket.ArrayBuffer.t()) :: :ok | {:error, term()}
@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).
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.
@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.
Returns connection state and bounded delivery and send queue usage.