ReverseProxyPlugWebsocket

A Plug for reverse proxying WebSocket connections to upstream servers.

Note: For HTTP reverse proxying, see reverse_proxy_plug.

Unlike traditional HTTP reverse proxying, WebSocket connections are bidirectional, stateful, and long-lived. This library handles the complexity of:

Why This Library?

HTTP reverse proxying fits naturally into Plug's request/response model. WebSocket proxying requires a different architecture:

HTTP Proxying WebSocket Proxying
Request → Response (stateless) Bidirectional persistent connection
Single direction flow Continuous message passing both ways
Fits Plug middleware model Requires protocol upgrade + stateful relay

This library bridges the gap, allowing you to use familiar Plug patterns for WebSocket reverse proxying.

Installation

Add reverse_proxy_plug_websocket to your list of dependencies in mix.exs.

You must also choose at least one WebSocket client adapter:

Option 1: Using Gun (Recommended)

def deps do
  [
    {:reverse_proxy_plug_websocket, "~> 0.1.0"},
    {:gun, "~> 2.1"}
  ]
end

Option 2: Using WebSockex

def deps do
  [
    {:reverse_proxy_plug_websocket, "~> 0.1.0"},
    {:websockex, "~> 0.4.3"}
  ]
end

Option 3: Both Adapters (Maximum Flexibility)

def deps do
  [
    {:reverse_proxy_plug_websocket, "~> 0.1.0"},
    {:gun, "~> 2.1"},
    {:websockex, "~> 0.4.3"}
  ]
end

The library will automatically use Gun if available, otherwise WebSockex. You can also explicitly specify which adapter to use in your configuration.

Usage

Basic Example

In your Phoenix endpoint or Plug router:

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # Proxy WebSocket connections to upstream
  plug ReverseProxyPlugWebsocket,
    upstream_uri: "wss://echo.websocket.org/"

  # Your other plugs...
end

With Authentication

Forward authentication headers using runtime configuration:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "wss://api.example.com/socket",
  headers: [{"authorization", "Bearer #{Application.get_env(:my_app, :api_token)}"}]

Secure Connections (WSS)

For secure WebSocket connections with custom TLS options:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "wss://secure.example.com/socket",
  tls_opts: [
    verify: :verify_peer,
    cacertfile: "/path/to/ca.pem",
    certfile: "/path/to/client-cert.pem",
    keyfile: "/path/to/client-key.pem"
  ]

WebSocket Subprotocols

Negotiate specific WebSocket subprotocols:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "ws://localhost:4000/socket",
  path: "/socket",
  protocols: ["mqtt", "v12.stomp"]

Custom Timeouts

Adjust connection and upgrade timeouts:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "ws://localhost:4000/socket",
  path: "/socket",
  connect_timeout: 10_000,  # 10 seconds to establish TCP connection
  upgrade_timeout: 15_000   # 15 seconds for WebSocket upgrade

Frame Processing

Frame processors let you intercept, modify, or drop WebSocket frames as they flow through the proxy — in either direction.

Both take a function (frame, state) -> frame | :skip:

Frame Types

Frame Direction Description
{:text, binary} both Text WebSocket frame
{:binary, binary} both Binary WebSocket frame
{:ping, binary} both Ping control frame
{:pong, binary} both Pong control frame
:close client → server Close frame (no code)
{:close, code, reason} server → client Close frame with status code

Examples

Log all frames in both directions:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "ws://localhost:4000/socket",
  path: "/socket",
  client_frame_processor: fn frame, _state ->
    Logger.debug("client → server: #{inspect(frame)}")
    frame
  end,
  server_frame_processor: fn frame, _state ->
    Logger.debug("server → client: #{inspect(frame)}")
    frame
  end

Transform text frames going to upstream:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "ws://localhost:4000/socket",
  path: "/socket",
  client_frame_processor: fn
    {:text, text}, _state -> {:text, String.upcase(text)}
    frame, _state -> frame
  end

Drop frames matching a pattern coming from upstream:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "ws://localhost:4000/socket",
  path: "/socket",
  server_frame_processor: fn
    {:text, "internal:" <> _}, _state -> :skip
    frame, _state -> frame
  end

Choosing an Adapter

The library supports two WebSocket client adapters:

Using Gun Adapter (Default)

Gun is the default adapter - no configuration needed:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "wss://echo.websocket.org/"

Or explicitly specify it:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "wss://echo.websocket.org/",
  adapter: ReverseProxyPlugWebsocket.Adapters.Gun

Using WebSockex Adapter

WebSockex is a pure Elixir alternative:

plug ReverseProxyPlugWebsocket,
  upstream_uri: "wss://echo.websocket.org/",
  adapter: ReverseProxyPlugWebsocket.Adapters.WebSockex

When to use WebSockex:

When to use Gun:

Configuration Options

Option Type Required Default Description
:upstream_uri String Yes - WebSocket URI to proxy to (ws:// or wss://)
:path String Yes - Path to proxy WebSocket requests from (e.g., "/socket")
:adapter Module No Auto-detected WebSocket client adapter (Gun or WebSockex). Defaults to Gun if available, otherwise WebSockex
:headers List No [] Additional headers to forward
:connect_timeout Integer No 5000 Connection timeout in milliseconds
:upgrade_timeout Integer No 5000 WebSocket upgrade timeout in ms
:protocols List No [] WebSocket subprotocols to negotiate
:tls_opts Keyword No [] TLS options for wss:// connections
:client_frame_processor Function No passthrough (frame, state) -> frame | :skip — intercept client → server frames
:server_frame_processor Function No passthrough (frame, state) -> frame | :skip — intercept server → client frames

Architecture

The library consists of several key components:

1. Main Plug (ReverseProxyPlugWebsocket)

2. WebSocket Handler (WebSocketHandler)

3. Proxy Process (ProxyProcess)

4. WebSocket Client (WebSocketClient behaviour)

5. WebSocket Client Adapters

Gun Adapter (Adapters.Gun)

WebSockex Adapter (Adapters.WebSockex)

How It Works

Client Browser          Plug Server              Upstream Server
     |                       |                          |
     |--- WS Upgrade ------->|                          |
     |                       |--- Connect Gun --------->|
     |                       |<-- WS Upgrade OK --------|
     |<-- WS Upgrade OK -----|                          |
     |                       |                          |
     |                  [ProxyProcess]                  |
     |                       |                          |
     |--- WS Frame --------->|--- Forward Frame ------->|
     |                       |                          |
     |<-- WS Frame ----------|<-- Forward Frame --------|
     |                       |                          |

Examples

Phoenix Router Integration

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :websocket do
    plug ReverseProxyPlugWebsocket,
      upstream_uri: "wss://echo.websocket.org/"
  end

  scope "/api" do
    pipe_through :websocket

    get "/socket", PageController, :index
  end
end

Conditional Proxying

Only proxy specific paths:

defmodule MyAppWeb.WebSocketProxy do
  import Plug.Conn

  def init(opts), do: opts

  def call(%{path_info: ["ws" | _]} = conn, _opts) do
    ReverseProxyPlugWebsocket.call(conn, [
      upstream_uri: "wss://echo.websocket.org/"
    ])
  end

  def call(conn, _opts), do: conn
end

Dynamic Upstream Selection

Choose upstream based on request:

defmodule MyAppWeb.DynamicProxy do
  def init(opts), do: opts

  def call(conn, _opts) do
    upstream = select_upstream(conn)

    ReverseProxyPlugWebsocket.call(conn, [
      upstream_uri: upstream
    ])
  end

  defp select_upstream(conn) do
    case get_req_header(conn, "x-region") do
      ["us-east"] -> "ws://us-east.backend.com/socket"
      ["eu-west"] -> "ws://eu-west.backend.com/socket"
      _ -> "ws://default.backend.com/socket"
    end
  end
end

Development

Clone the repository and install dependencies:

git clone https://github.com/mwhitworth/reverse_proxy_plug_websocket.git
cd reverse_proxy_plug_websocket
mix deps.get

Run tests:

mix test

Generate documentation:

mix docs

Testing

The library includes comprehensive tests for:

Note: Integration tests require a running WebSocket server. See test/ directory for examples.

Limitations