# ReverseProxyPlugWebsocket
A Plug for reverse proxying WebSocket connections to upstream servers.
> **Note:** For HTTP reverse proxying, see [reverse_proxy_plug](https://hex.pm/packages/reverse_proxy_plug).
Unlike traditional HTTP reverse proxying, WebSocket connections are bidirectional, stateful, and long-lived. This library handles the complexity of:
- Detecting WebSocket upgrade requests
- Establishing connections to upstream WebSocket servers
- Maintaining bidirectional message flow between clients and upstream
- Managing connection lifecycle and cleanup
## 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)
```elixir
def deps do
[
{:reverse_proxy_plug_websocket, "~> 0.1.0"},
{:gun, "~> 2.1"}
]
end
```
### Option 2: Using WebSockex
```elixir
def deps do
[
{:reverse_proxy_plug_websocket, "~> 0.1.0"},
{:websockex, "~> 0.4.3"}
]
end
```
### Option 3: Both Adapters (Maximum Flexibility)
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
plug ReverseProxyPlugWebsocket,
upstream_uri: "ws://localhost:4000/socket",
path: "/socket",
protocols: ["mqtt", "v12.stomp"]
```
### Custom Timeouts
Adjust connection and upgrade timeouts:
```elixir
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
```
### Choosing an Adapter
The library supports two WebSocket client adapters:
#### Using Gun Adapter (Default)
Gun is the default adapter - no configuration needed:
```elixir
plug ReverseProxyPlugWebsocket,
upstream_uri: "wss://echo.websocket.org/"
```
Or explicitly specify it:
```elixir
plug ReverseProxyPlugWebsocket,
upstream_uri: "wss://echo.websocket.org/",
adapter: ReverseProxyPlugWebsocket.Adapters.Gun
```
#### Using WebSockex Adapter
WebSockex is a pure Elixir alternative:
```elixir
plug ReverseProxyPlugWebsocket,
upstream_uri: "wss://echo.websocket.org/",
adapter: ReverseProxyPlugWebsocket.Adapters.WebSockex
```
**When to use WebSockex:**
- You prefer pure Elixir dependencies
- Simpler debugging and error messages
- Don't need HTTP/2 support
- Want easier extensibility
**When to use Gun:**
- Need HTTP/2 support
- Want battle-tested production stability
- Require advanced connection pooling
## 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 |
## Architecture
The library consists of several key components:
### 1. Main Plug (`ReverseProxyPlugWebsocket`)
- Detects WebSocket upgrade requests
- Validates configuration
- Initiates WebSocket upgrade
### 2. WebSocket Handler (`WebSocketHandler`)
- Manages client-side WebSocket connection
- Implements `WebSock` behaviour
- Coordinates with ProxyProcess
### 3. Proxy Process (`ProxyProcess`)
- GenServer managing bidirectional relay
- Maintains both client and upstream connections
- Handles message forwarding and lifecycle
### 4. WebSocket Client (`WebSocketClient` behaviour)
- Defines adapter interface
- Allows multiple client implementations
### 5. WebSocket Client Adapters
#### Gun Adapter (`Adapters.Gun`)
- Default adapter using `:gun` Erlang library
- Robust HTTP/2 and WebSocket support
- Battle-tested in production environments
#### WebSockex Adapter (`Adapters.WebSockex`)
- Pure Elixir WebSocket client
- Simple callback-based API
- RFC6455 compliant
- Easier to debug and extend
## 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
```elixir
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:
```elixir
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:
```elixir
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:
```bash
git clone https://github.com/mwhitworth/reverse_proxy_plug_websocket.git
cd reverse_proxy_plug_websocket
mix deps.get
```
Run tests:
```bash
mix test
```
Generate documentation:
```bash
mix docs
```
## Testing
The library includes comprehensive tests for:
- Configuration validation
- WebSocket upgrade detection
- Header forwarding
- Connection lifecycle
Note: Integration tests require a running WebSocket server. See `test/` directory for examples.
## Limitations
- Requires Cowboy or Bandit as the web server
- WebSocket compression is not yet supported