### Implement WebSocket Server and Plug Router Source: https://github.com/phoenixframework/websock_adapter/blob/main/README.md This example demonstrates how to implement a WebSocket server using a GenServer-like module and integrate it into a Plug router for handling WebSocket upgrades. ```elixir defmodule EchoServer do def init(args) do {:ok, []} end def handle_in({"ping", [opcode: :text]}, state) do {:reply, :ok, {:text, "pong"}, state} end end defmodule MyPlug do use Plug.Router plug Plug.Logger plug :match plug :dispatch get "/" do # Provide the user with some useful instructions to copy & paste into their inspector send_resp(conn, 200, """ Use the JavaScript console to interact using websockets sock = new WebSocket(\"ws://localhost:4000/websocket\") sock.addEventListener(\"message\", console.log) sock.addEventListener(\"open\", () => sock.send(\"ping\")) """) end get "/websocket" do conn |> WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000) |> halt() end match _ do send_resp(conn, 404, "not found") end end ``` -------------------------------- ### Implement WebSock Behaviour Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Provides a complete example of a WebSock handler implementing init, handle_in, handle_control, handle_info, and terminate callbacks. ```elixir defmodule ChatServer do @behaviour WebSock # Called after successful WebSocket upgrade # Returns {:ok, state} to accept the connection @impl true def init(args) do # args is the state passed to upgrade/4 {:ok, %{room: args[:room], username: args[:username], messages: []}} end # Handle incoming text and binary frames # opcode is :text or :binary @impl true def handle_in({message, [opcode: :text]}, state) do case Jason.decode(message) do {:ok, %{"type" => "chat", "content" => content}} -> response = %{type: "chat", from: state.username, content: content} {:reply, :ok, {:text, Jason.encode!(response)}, state} {:ok, %{"type" => "ping"}} -> {:reply, :ok, {:text, Jason.encode!(%{type: "pong"})}, state} {:error, _} -> {:ok, state} # Ignore invalid JSON end end def handle_in({_data, [opcode: :binary]}, state) do # Handle binary data {:ok, state} end # Handle control frames (ping/pong) - optional callback @impl true def handle_control({_payload, [opcode: :ping]}, state) do # Pong is sent automatically, but you can add custom logic {:ok, state} end def handle_control({_payload, [opcode: :pong]}, state) do {:ok, state} end # Handle Erlang messages sent to the WebSocket process @impl true def handle_info({:broadcast, message}, state) do {:push, {:text, Jason.encode!(message)}, state} end def handle_info(:heartbeat, state) do {:push, {:text, Jason.encode!(%{type: "heartbeat"})}, state} end def handle_info(_message, state) do {:ok, state} end # Called when connection closes - optional callback @impl true def terminate(:normal, state) do IO.puts("User #{state.username} disconnected normally") :ok end def terminate(:remote, state) do IO.puts("User #{state.username} closed connection") :ok end def terminate({:error, reason}, state) do IO.puts("User #{state.username} disconnected with error: #{inspect(reason)}") :ok end end ``` -------------------------------- ### Configure WebSocket Connection Options Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Demonstrates how to configure various WebSocket connection options, such as timeouts, compression, frame size limits, and garbage collection tuning. These options are passed to `WebSockAdapter.upgrade/4` to control connection behavior. ```elixir # All available connection options connection_opts = [ # Timeout in milliseconds before closing idle connections (default: 60_000) timeout: 120_000, # Enable permessage-deflate compression (default: false) compress: true, # Maximum frame size in bytes (default: :infinity) max_frame_size: 65_536, # Validate UTF-8 in text frames (default: true) validate_utf8: true, # Garbage collection tuning (requires OTP 24+) fullsweep_after: 10, # Maximum process heap size max_heap_size: %{size: 1_000_000, kill: true, error_logger: true}, # Cowboy-specific: packets to request from socket at once active_n: 100, # Deflate compression options deflate_options: [level: :best_compression], # Skip WebSockAdapter's early validation (default: true) early_validate_upgrade: false ] # Usage with all options get "/ws" do conn |> WebSockAdapter.upgrade(MyHandler, initial_state, connection_opts) |> halt() end ``` -------------------------------- ### Define and Use a WebSock Handler Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Defines a WebSock handler module implementing the WebSock behavior and demonstrates its usage within a Plug router to upgrade connections. Ensure the handler module implements the required callbacks like `init`, `handle_in`, `handle_info`, and `terminate`. ```elixir defmodule EchoServer do @behaviour WebSock @impl true def init(args) do {:ok, %{messages: [], opts: args}} end @impl true def handle_in({"ping", [opcode: :text]}), do: {:reply, :ok, {:text, "pong"}, state} def handle_in({message, [opcode: :text]}), do: {:reply, :ok, {:text, "Echo: #{message}"}, %{state | messages: [message | state.messages]}} def handle_in({data, [opcode: :binary]}), do: {:reply, :ok, {:binary, data}, state} @impl true def handle_info(message, state) do {:push, {:text, "Info: #{inspect(message)}"}, state} end @impl true def terminate(:normal, _state), do: :ok def terminate(:remote, _state), do: :ok def terminate(_reason, _state), do: :ok end defmodule MyRouter do use Plug.Router plug Plug.Logger plug :match plug :dispatch get "/" do send_resp(conn, 200, """ Use the JavaScript console to interact using websockets sock = new WebSocket(\"ws://localhost:4000/websocket\") sock.addEventListener(\"message\", console.log) sock.addEventListener(\"open\", () => sock.send(\"ping\")) """) end get "/websocket" do conn |> WebSockAdapter.upgrade(EchoServer, %{user_id: 123}, timeout: 60_000) |> halt() end get "/websocket/compressed" do conn |> WebSockAdapter.upgrade(EchoServer, %{}, timeout: 120_000, compress: true, max_frame_size: 1_048_576, validate_utf8: true ) |> halt() end match _ do send_resp(conn, 404, "not found") end end # Start the server with Bandit Bandit.start_link(plug: MyRouter, port: 4000) # Or with Cowboy Plug.Cowboy.http(MyRouter, [], port: 4000) ``` -------------------------------- ### Add websock_adapter to Mix Dependencies Source: https://github.com/phoenixframework/websock_adapter/blob/main/README.md Instructions for adding the websock_adapter to your project's dependencies in the mix.exs file. ```elixir def deps do [ {:websock_adapter, "~> 0.5"} ] end ``` -------------------------------- ### Connection Options Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Configure WebSocket connection behavior including timeouts, compression, frame size limits, and process flags. These options are passed to `WebSockAdapter.upgrade/4` and control how the WebSocket connection operates after upgrade. ```APIDOC ## Connection Options ### Description Configure WebSocket connection behavior including timeouts, compression, frame size limits, and process flags. These options are passed to `WebSockAdapter.upgrade/4` and control how the WebSocket connection operates after upgrade. ### Available Options - **timeout** (integer) - Timeout in milliseconds before closing idle connections (default: `60_000`). - **compress** (boolean) - Enable permessage-deflate compression (default: `false`). - **max_frame_size** (integer) - Maximum frame size in bytes (default: `:infinity`). - **validate_utf8** (boolean) - Validate UTF-8 in text frames (default: `true`). - **fullsweep_after** (integer) - Garbage collection tuning (requires OTP 24+) (default: not specified). - **max_heap_size** (map) - Maximum process heap size. Example: `%{size: 1_000_000, kill: true, error_logger: true}`. - **active_n** (integer) - Cowboy-specific: packets to request from socket at once (default: not specified). - **deflate_options** (keyword list) - Deflate compression options. Example: `[level: :best_compression]`. - **early_validate_upgrade** (boolean) - Skip WebSockAdapter's early validation (default: `true`). ### Usage Example ```elixir get "/ws" do conn |> WebSockAdapter.upgrade(MyHandler, initial_state, [ timeout: 120_000, compress: true, max_frame_size: 65_536, validate_utf8: true, fullsweep_after: 10, max_heap_size: %{size: 1_000_000, kill: true, error_logger: true}, active_n: 100, deflate_options: [level: :best_compression], early_validate_upgrade: false ]) |> halt() end ``` ``` -------------------------------- ### WebSockAdapter.upgrade/4 Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Upgrades a Plug connection to a WebSocket connection using the specified WebSock handler module. This is the primary entry point for establishing WebSocket connections, handling request validation, and configuring connection options. ```APIDOC ## WebSockAdapter.upgrade/4 ### Description Upgrades a Plug connection to a WebSocket connection using the specified WebSock handler module. This is the primary entry point for establishing WebSocket connections, handling request validation, and configuring connection options like timeouts, compression, and frame size limits. ### Method POST (or GET, depending on server implementation) ### Endpoint `/websocket` (example path) ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None (The upgrade process is typically initiated via HTTP headers) ### Request Example This endpoint is typically called within a Plug router. The request itself is an HTTP request that gets upgraded. ### Response #### Success Response (101 Switching Protocols) - **Connection** (string) - `upgrade` - **Upgrade** (string) - `websocket` - **Sec-WebSocket-Accept** (string) - The computed accept key for the WebSocket handshake. #### Response Example ``` HTTP/1.1 101 Switching Protocols Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: s3pPLMBiTxaQ9k2x0Y02z0j6t00= ``` ### Example Usage in Plug Router ```elixir defmodule MyRouter do use Plug.Router plug Plug.Logger plug :match plug :dispatch get "/websocket" do conn |> WebSockAdapter.upgrade(EchoServer, %{user_id: 123}, timeout: 60_000) |> halt() end # ... other routes end ``` ### Example WebSock Handler Module ```elixir defmodule EchoServer do @behaviour WebSock @impl true def init(args) do {:ok, %{messages: [], opts: args}} end @impl true def handle_in({"ping", [opcode: :text]}, state) do {:reply, :ok, {:text, "pong"}, state} end def handle_in({message, [opcode: :text]}, state) do {:reply, :ok, {:text, "Echo: #{message}"}}, %{state | messages: [message | state.messages]} end def handle_in({data, [opcode: :binary]}, state) do {:reply, :ok, {:binary, data}, state} end @impl true def handle_info(message, state) do {:push, {:text, "Info: #{inspect(message)}"}}, state end @impl true def terminate(:normal, _state), do: :ok def terminate(:remote, _state), do: :ok def terminate(_reason, _state), do: :ok end ``` ``` -------------------------------- ### Handle WebSockAdapter.UpgradeError Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Shows how to rescue UpgradeError to log validation failures and return custom error responses. ```elixir defmodule MyRouter do use Plug.Router plug :match plug :dispatch get "/websocket" do try do conn |> WebSockAdapter.upgrade(EchoServer, %{}, []) |> halt() rescue e in WebSockAdapter.UpgradeError -> # Log the specific validation failure require Logger Logger.warning("WebSocket upgrade failed: #{e.message}") # Return appropriate error response conn |> put_resp_content_type("text/plain") |> send_resp(400, "Invalid WebSocket upgrade request: #{e.message}") end end end ``` -------------------------------- ### Implement WebSock Handler Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Defines a handler module implementing the WebSock behaviour to manage connection lifecycle, incoming messages, and PubSub integration. ```elixir # Stream Handler defmodule MyAppWeb.StreamHandler do @behaviour WebSock @impl true def init(state) do # Subscribe to PubSub for real-time updates Phoenix.PubSub.subscribe(MyApp.PubSub, "stream:#{state.stream_id}") {:ok, state} end @impl true def handle_in({message, [opcode: :text]}, state) do # Handle incoming messages from client case Jason.decode(message) do {:ok, data} -> process_client_message(data, state) {:error, _} -> {:ok, state} end end @impl true def handle_info({:stream_update, data}, state) do # Forward PubSub messages to WebSocket client {:push, {:text, Jason.encode!(data)}, state} end def handle_info(_msg, state), do: {:ok, state} defp process_client_message(%{"action" => "subscribe", "topic" => topic}, state) do Phoenix.PubSub.subscribe(MyApp.PubSub, topic) {:reply, :ok, {:text, Jason.encode!(%{subscribed: topic})}, state} end defp process_client_message(_, state), do: {:ok, state} end ``` -------------------------------- ### Integrate WebSockAdapter in Phoenix Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Configures a custom WebSocket route in the Phoenix endpoint and router, and implements a controller to upgrade the connection using WebSockAdapter. ```elixir # In your Phoenix Endpoint defmodule MyAppWeb.Endpoint do use Phoenix.Endpoint, otp_app: :my_app # Add a custom WebSocket route before the router plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library() plug MyAppWeb.Router end # In your Phoenix Router defmodule MyAppWeb.Router do use MyAppWeb, :router pipeline :api do plug :accepts, ["json"] end # Custom WebSocket endpoint using WebSockAdapter scope "/api", MyAppWeb do pipe_through :api get "/ws/stream", WebSocketController, :stream end end # WebSocket Controller defmodule MyAppWeb.WebSocketController do use MyAppWeb, :controller def stream(conn, params) do user = conn.assigns[:current_user] conn |> WebSockAdapter.upgrade(MyAppWeb.StreamHandler, %{ user_id: user.id, stream_id: params["stream_id"] }, timeout: 300_000, compress: true ) |> halt() end end ``` -------------------------------- ### Validate WebSocket Upgrades in Elixir Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Demonstrates manual validation using validate_upgrade/1 and the raising variant validate_upgrade!/1 within a Plug router. ```elixir defmodule MyRouter do use Plug.Router plug :match plug :dispatch get "/websocket" do # Manual validation with custom error handling case WebSockAdapter.UpgradeValidation.validate_upgrade(conn) do :ok -> conn |> WebSockAdapter.upgrade(EchoServer, %{}, early_validate_upgrade: false) |> halt() {:error, reason} -> conn |> put_resp_content_type("application/json") |> send_resp(400, Jason.encode!(%{error: "WebSocket upgrade failed", reason: reason})) end end end # Using the raising variant for simple error handling defmodule StrictRouter do use Plug.Router plug :match plug :dispatch get "/websocket" do # Raises WebSockAdapter.UpgradeError if validation fails WebSockAdapter.UpgradeValidation.validate_upgrade!(conn) conn |> WebSockAdapter.upgrade(EchoServer, %{}, early_validate_upgrade: false) |> halt() end end ``` -------------------------------- ### WebSock Handler Callbacks Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Defines the callbacks required for implementing a WebSocket handler module using the `@behaviour WebSock`. ```APIDOC ## WebSock Handler Callbacks ### Description Implement the `WebSock` behaviour to handle WebSocket events. The handler module receives messages, manages state, and controls the connection lifecycle through a set of callbacks. ### Callbacks #### `init(args)` - **Description**: Called after a successful WebSocket upgrade. - **Returns**: `{:ok, state}` to accept the connection. - **Arguments**: `args` - The state passed to `upgrade/4`. #### `handle_in({message, [opcode: :text]}, state)` - **Description**: Handles incoming text frames. - **Returns**: `{:reply, status, frame, state}` or `{:ok, state}`. - **Arguments**: `message` (string), `state` (any). #### `handle_in({data, [opcode: :binary]}, state)` - **Description**: Handles incoming binary frames. - **Returns**: `{:ok, state}`. - **Arguments**: `data` (binary), `state` (any). #### `handle_control({payload, [opcode: :ping]}, state)` (Optional) - **Description**: Handles incoming ping control frames. - **Returns**: `{:ok, state}`. - **Arguments**: `payload` (binary), `state` (any). #### `handle_control({payload, [opcode: :pong]}, state)` (Optional) - **Description**: Handles incoming pong control frames. - **Returns**: `{:ok, state}`. - **Arguments**: `payload` (binary), `state` (any). #### `handle_info({:broadcast, message}, state)` - **Description**: Handles Erlang messages sent to the WebSocket process for broadcasting. - **Returns**: `{:push, frame, state}`. - **Arguments**: `message` (any), `state` (any). #### `handle_info(:heartbeat, state)` - **Description**: Handles a heartbeat message. - **Returns**: `{:push, frame, state}`. - **Arguments**: `state` (any). #### `handle_info(message, state)` - **Description**: Handles other Erlang messages. - **Returns**: `{:ok, state}`. - **Arguments**: `message` (any), `state` (any). #### `terminate(reason, state)` (Optional) - **Description**: Called when the connection closes. - **Arguments**: `reason` (atom or tuple), `state` (any). ### Return Value Options for Handlers - `{:ok, state}`: Continue with the updated state. - `{:reply, status, frame, state}`: Send a frame and continue. - `{:push, frame, state}`: Send a frame (same as reply). ### Request Example ```elixir defmodule ChatServer do @behaviour WebSock # Called after successful WebSocket upgrade # Returns {:ok, state} to accept the connection @impl true def init(args) do # args is the state passed to upgrade/4 {:ok, %{room: args[:room], username: args[:username], messages: []}} end # Handle incoming text and binary frames # opcode is :text or :binary @impl true def handle_in({message, [opcode: :text]}, state) do case Jason.decode(message) do {:ok, %{"type" => "chat", "content" => content}} -> response = %{type: "chat", from: state.username, content: content} {:reply, :ok, {:text, Jason.encode!(response)}, state} {:ok, %{"type" => "ping"}} -> {:reply, :ok, {:text, Jason.encode!(%{type: "pong"})}, state} {:error, _} -> {:ok, state} # Ignore invalid JSON end end def handle_in({_data, [opcode: :binary]}, state) do # Handle binary data {:ok, state} end # Handle control frames (ping/pong) - optional callback @impl true def handle_control({_payload, [opcode: :ping]}, state) do # Pong is sent automatically, but you can add custom logic {:ok, state} end def handle_control({_payload, [opcode: :pong]}, state) do {:ok, state} end # Handle Erlang messages sent to the WebSocket process @impl true def handle_info({:broadcast, message}, state) do {:push, {:text, Jason.encode!(message)}, state} end def handle_info(:heartbeat, state) do {:push, {:text, Jason.encode!(%{type: "heartbeat"})}, state} end def handle_info(_message, state) do {:ok, state} end # Called when connection closes - optional callback @impl true def terminate(:normal, state) do IO.puts("User #{state.username} disconnected normally") :ok end def terminate(:remote, state) do IO.puts("User #{state.username} closed connection") :ok end def terminate({:error, reason}, state) do IO.puts("User #{state.username} disconnected with error: #{inspect(reason)}") :ok end end ``` ``` -------------------------------- ### WebSockAdapter.UpgradeError Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Exception raised when WebSocket upgrade validation fails. Contains detailed information about why the upgrade request was invalid. ```APIDOC ## POST /websocket ### Description Handles exceptions raised by `WebSockAdapter.UpgradeValidation.validate_upgrade/1` when a WebSocket upgrade request is invalid. ### Method GET ### Endpoint /websocket ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None ### Request Example ```elixir defmodule MyRouter do use Plug.Router plug :match plug :dispatch get "/websocket" do try do conn |> WebSockAdapter.upgrade(EchoServer, %{}, []) |> halt() rescue e in WebSockAdapter.UpgradeError -> # Log the specific validation failure require Logger Logger.warning("WebSocket upgrade failed: #{e.message}") # Return appropriate error response conn |> put_resp_content_type("text/plain") |> send_resp(400, "Invalid WebSocket upgrade request: #{e.message}") end end end ``` ### Response #### Success Response (200) - **conn** (Plug.Conn) - The connection object after a successful upgrade. #### Error Response (400) - **message** (string) - A message indicating the WebSocket upgrade request was invalid and the reason. #### Response Example ``` Invalid WebSocket upgrade request: HTTP method POST unsupported (must be GET for HTTP/1.1) ``` ### Common Error Messages - "HTTP method POST unsupported" (must be GET for HTTP/1.1) - "HTTP version HTTP/1.0 unsupported" - "'connection' header must contain 'upgrade'" - "'upgrade' header must contain 'websocket'" - "'sec-websocket-version' header must equal '13'" - "'host' header is absent" - "'sec-websocket-key' header is absent" ``` -------------------------------- ### WebSockAdapter.UpgradeValidation.validate_upgrade/1 Source: https://context7.com/phoenixframework/websock_adapter/llms.txt Validates WebSocket upgrade requests according to RFC specifications. Can be called manually for custom validation flows. ```APIDOC ## POST /websocket ### Description Validates that a request satisfies WebSocket upgrade requirements per RFC6455§4.2 for HTTP/1.1 and RFC8441§5 for HTTP/2. This is automatically called by `upgrade/4` unless disabled, but can be called manually for custom validation flows. ### Method GET ### Endpoint /websocket ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None ### Request Example ```elixir defmodule MyRouter do use Plug.Router plug :match plug :dispatch get "/websocket" do # Manual validation with custom error handling case WebSockAdapter.UpgradeValidation.validate_upgrade(conn) do :ok -> conn |> WebSockAdapter.upgrade(EchoServer, %{}, early_validate_upgrade: false) |> halt() {:error, reason} -> conn |> put_resp_content_type("application/json") |> send_resp(400, Jason.encode!(%{error: "WebSocket upgrade failed", reason: reason})) end end end # Using the raising variant for simple error handling defmodule StrictRouter do use Plug.Router plug :match plug :dispatch get "/websocket" do # Raises WebSockAdapter.UpgradeError if validation fails WebSockAdapter.UpgradeValidation.validate_upgrade!(conn) conn |> WebSockAdapter.upgrade(EchoServer, %{}, early_validate_upgrade: false) |> halt() end end ``` ### Response #### Success Response (200) - **conn** (Plug.Conn) - The connection object after a successful upgrade. #### Response Example None provided. ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.