# `HTTP.WebSocket`
[🔗](https://github.com/gsmlg-dev/http_fetch/blob/v1.0.0-rc.4/apps/http_web_socket/lib/http/web_socket.ex#L1)

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.

# `handshake`

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

# `t`

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

# `acknowledge`

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

Acknowledges an ACK-mode message delivery reference.

# `array_buffer`

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

# `binary_type`

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

# `buffered_amount`

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

# `close`

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

# `close`

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

# `close`

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

# `closed`

```elixir
@spec closed() :: 3
```

# `closing`

```elixir
@spec closing() :: 2
```

# `completion`

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

Returns the durable cleanup receipt without contacting the connection.

# `connecting`

```elixir
@spec connecting() :: 0
```

# `extensions`

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

# `http_version`

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

Returns the negotiated HTTP version, or nil before establishment.

# `new`

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

# `open`

```elixir
@spec open() :: 1
```

# `protocol`

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

# `ready_state`

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

# `send`

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

# `send_ack`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

# `status`

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

Returns connection state and bounded delivery and send queue usage.

# `url`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
