ReverseIt - Elixir HTTP/WebSocket Reverse Proxy

A full-featured HTTP/1.1, optional HTTP/2, and WebSocket reverse proxy for Elixir, built using Finch (HTTP) and Mint (WebSockets). Designed to work seamlessly within Phoenix/Plug pipelines.

Features

Setup

First, add ReverseIt to your application's supervision tree with a connection pool:

defmodule MyApp.Application do
def start(_type, _args) do
children = [
# Start ReverseIt with a connection pool
{ReverseIt, name: MyApp.ReverseProxy, pool_size: 100},
# ... other children
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end

Usage

In a Phoenix Router

defmodule MyAppWeb.Router do
use MyAppWeb, :router
# Regular Phoenix routes
scope "/", MyAppWeb do
get "/", PageController, :index
end
# Proxy API requests to backend service
scope "/api" do
forward "/", ReverseIt,
name: MyApp.ReverseProxy,
backend: "http://backend-api:4000",
strip_path: "/api"
end
# Proxy WebSocket connections
scope "/socket" do
forward "/", ReverseIt,
name: MyApp.ReverseProxy,
backend: "ws://backend-ws:4000"
end
end

As a Plug

defmodule MyApp.ProxyPlug do
use Plug.Router
plug :match
plug :dispatch
forward "/", ReverseIt,
name: MyApp.ReverseProxy,
backend: "http://localhost:4001",
upstream_idle_timeout: 60_000,
protocols: [:http1, :http2]
end

One-Shot Unix-Socket Upstreams

Use a one-shot upstream when every proxied request or WebSocket upgrade must receive a fresh connection to a local Unix-domain socket:

ReverseIt.call(
conn,
ReverseIt.init(
name: MyApp.ReverseProxy,
backend: "http://provider-tunnel",
unix_socket: "/run/my_app/provider-tunnel.sock",
upstream_connection: :one_shot,
protocols: [:http1]
)
)

The backend host remains the HTTP Host and WebSocket authority while :unix_socket selects the transport address. Unix-socket upstreams are local, unencrypted HTTP/1.1 only and always one-shot; ReverseIt never returns these connections to the Finch pool.

Customizing Requests and Responses

You can wrap ReverseIt in your own Plug to modify request headers, add response headers, implement authentication, logging, etc. Use Plug.Conn.register_before_send/2 to modify responses before they're sent to the client.

defmodule MyApp.APIProxy do
@moduledoc """
Custom proxy that adds authentication and custom headers.
"""
@behaviour Plug
def init(opts), do: opts
def call(conn, _opts) do
# Modify request before proxying
conn
|> Plug.Conn.put_req_header("x-api-key", "...")
# Register callback to modify response after backend responds
|> Plug.Conn.register_before_send(fn conn ->
conn
|> Plug.Conn.put_resp_header("x-proxy-by", "MyApp")
|> Plug.Conn.put_resp_header("x-proxy-version", "1.0")
|> log_request()
end)
# Proxy to backend
|> ReverseIt.call(
ReverseIt.init(
name: MyApp.ReverseProxy,
backend: "http://backend-api:4000",
strip_path: "/api"
)
)
end
defp log_request(conn) do
Logger.info("Proxied #{conn.method} #{conn.request_path}#{conn.status}")
conn
end
end
# In your router:
scope "/api" do
forward "/", MyApp.APIProxy
end

Configuration Options

Supervisor Options (when starting ReverseIt)

Plug Options (when using as a Plug)

Router-Style Defaults

ReverseIt’s defaults are intentionally broad enough for general HTTP routers while still bounding common DoS vectors:

If ReverseIt runs behind a trusted edge proxy, set forwarded_headers: :replace at the edge-facing ReverseIt instance. Use :append only when downstream applications treat X-Forwarded-* as informational rather than trusted identity.

Testing

The project includes comprehensive test coverage with test servers that start automatically during test runs:

# Run all tests
# Test servers start automatically on available local ports
mix test
# Run only WebSocket tests
mix test --only websocket

Note: Test servers are only started during mix test and are not included in the library when used as a dependency.

Interactive Testing

For manual/interactive testing, the example clients can be used while tests are running:

# Terminal 1: Keep test servers running
mix test --trace
# Terminal 2: Run example clients
node examples/node_client.js
python3 examples/python_client.py
# Or use curl/wscat
curl http://localhost:4000/hello
wscat -c ws://localhost:4000/ws

Example Clients

The examples/ directory contains full test clients in multiple languages:

# Node.js client (requires: npm install ws)
node examples/node_client.js
# Python client (requires: pip install requests websocket-client)
python3 examples/python_client.py
# Quick curl examples
bash examples/curl_examples.sh

See examples/README.md for detailed usage.

Architecture

HTTP Proxy Flow

Client Phoenix/Bandit ReverseIt (Plug)Finch (connection pool)Backend
50 pooled HTTP/1.1 connections by default

For upstream_connection: :one_shot, ReverseIt uses a fresh passive Mint HTTP/1.1 connection instead of Finch. This path supports both TCP and Unix-domain sockets and closes the upstream after the response.

WebSocket Proxy Flow

Client Phoenix/Bandit ReverseIt (Plug)ReverseIt.WebSocketProxy (WebSock)Mint.WebSocket Backend

Connection Pooling

ReverseIt uses Finch for HTTP requests, providing:

You configure the pool when adding ReverseIt to your supervisor tree:

children = [
{ReverseIt, name: MyApp.ReverseProxy, pool_size: 100, pool_count: 2}
]

Project Structure

lib/
├── reverse_it.ex # Main Plug module with protocol detection
└── reverse_it/
├── application.ex # OTP application supervisor
├── config.ex # Configuration parser and validator
├── http_proxy.ex # HTTP request proxying logic
├── upstream.ex # TCP/Unix one-shot Mint connections
└── websocket_proxy.ex # WebSocket proxy handler (WebSock behavior)
test/
└── support/
├── test_backend.ex # Test backend server
└── test_proxy.ex # Test proxy server

Implementation Status

HTTP Proxying:

WebSocket Proxying: