# `ReverseIt`
[🔗](https://github.com/chrismccord/reverse_it/blob/main/lib/reverse_it.ex#L1)

A hardened HTTP/1.1, optional HTTP/2, and WebSocket reverse proxy for Elixir.

Built using Finch (HTTP) and Mint (WebSockets), ReverseIt is designed to work seamlessly within
Phoenix/Plug pipelines as a standard Plug module.

WebSocket proxying currently supports Bandit. Cowboy's WebSocket process handoff is not
supported: the upstream socket belongs to the HTTP request process and can close during
the upgrade. ReverseIt logs a warning on Cowboy WebSocket attempts; use Bandit for
WebSocket routes. Ordinary HTTP proxying is unaffected.

Streamed downloads preserve the backend's `Content-Length` without buffering the
complete response. Use Bandit 1.12.2 or newer when response compression is enabled
so the server does not compress a length-delimited stream and invalidate its length.

## Features

- **Full HTTP Support**: HTTP/1.1 proxying by default, optional HTTP/2 upstreams, and streaming request/response bodies
- **Connection Pooling**: Uses Finch for automatic connection pooling and reuse across requests
- **One-Shot Upstreams**: Fresh HTTP/1.1 connections over TCP or Unix-domain sockets
- **HTTP/2 Support**: Opt-in upstream HTTP/2 support with `protocols: [:http1, :http2]`
- **WebSocket Proxying**: Bidirectional WebSocket frame forwarding with full protocol support
- **Plug Integration**: Works as a standard Plug module in any Phoenix or Plug application
- **Header Management**: Automatic X-Forwarded-* header injection and hop-by-hop header filtering
- **DoS Protection**: Configurable request body, header, timeout, response, and WebSocket limits
- **Path Manipulation**: Strip path prefixes and add backend path prefixes
- **Protocol Detection**: Automatic detection and routing for HTTP vs WebSocket upgrades

## 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

    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]
      )
    )

`:unix_socket` selects the transport address while the backend host remains
the HTTP authority. Unix-socket upstreams support local, unencrypted HTTP/1.1
and WebSocket traffic. They always use a fresh connection and never enter the
Finch pool.

## Configuration Options

### Supervisor Options (when starting ReverseIt)

  * `:name` (required) - Name for the Finch connection pool
  * `:pool_size` - Max connections per backend (default: 50)
  * `:pool_count` - Number of connection pools (default: 1)
  * `:connect_timeout` - Backend connection timeout in ms (default: 5_000)
  * `:conn_max_idle_time` - Idle timeout for pooled backend HTTP/1 connections (default: 90_000)
  * `:protocols` - Upstream protocols for pooled Finch requests (default: [:http1])

### Plug Options (when using as a Plug)

  * `:name` (required) - Name of the Finch pool to use
  * `:backend` (required) - Backend URL (http://, https://, ws://, or wss://)
  * `:connect_ip` - Vetted IPv4/IPv6 tuple to dial while retaining the backend hostname for TLS and HTTP; requires `:one_shot` and cannot be combined with `:unix_socket`
  * `:unix_socket` - Connect through this Unix-domain socket instead of the backend host/port
  * `:upstream_connection` - `:pooled` or `:one_shot` (default: `:pooled`)
  * `:strip_path` - Path prefix to strip from incoming requests before proxying
  * `:connect_timeout` - Backend connection timeout in milliseconds (default: 5_000)
  * `:pool_timeout` - Finch pool checkout timeout in milliseconds (default: 5_000)
  * `:response_header_timeout` - Time to wait for backend response headers in streaming paths (default: 30_000)
  * `:upstream_idle_timeout` - Rolling idle timeout while receiving backend data (default: 55_000)
  * `:request_body_read_timeout` - Rolling timeout while reading client request bodies (default: 55_000)
  * `:max_request_body_size` - Maximum request body size in bytes (default: 100MB, `:infinity` for unlimited)
  * `:request_body_buffer_size` - Bytes buffered before switching to request streaming (default: 1MB)
  * `:max_response_body_size` - Maximum response body size in bytes (default: :infinity)
  * `:max_request_target_bytes` - Maximum request path/query bytes (default: 8KB)
  * `:max_request_header_line_bytes` - Maximum single request header bytes (default: 8KB)
  * `:max_request_header_bytes` - Maximum total request header bytes (default: 64KB)
  * `:max_request_headers` - Maximum request header count (default: 100)
  * `:max_response_header_bytes` - Maximum backend response header bytes (default: 64KB)
  * `:forwarded_headers` - `:append`, `:replace`, or `false` for X-Forwarded-* behavior (default: :append)
  * `:protocols` - List of supported upstream protocols (default: [:http1])
  * `:max_websocket_frame_size` - Maximum WebSocket frame/message size (default: 16MB)

## Connection Pooling

ReverseIt uses Finch for HTTP requests, which provides automatic connection pooling:

- **Pool Size**: 50 connections per backend by default
- **Reuse**: Connections are automatically reused across requests
- **HTTP/2 Support**: Upstream HTTP/2 can be enabled with `protocols: [:http1, :http2]`
- **Performance**: Eliminates TCP/TLS handshake overhead for subsequent requests

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

## Examples

### Basic HTTP Proxying

    # Proxy all requests to a backend server
    forward "/", ReverseIt,
      name: MyApp.ReverseProxy,
      backend: "http://localhost:4001"

### Path Stripping

    # Strip /api prefix before forwarding
    # Request to /api/users becomes /users at backend
    forward "/api", ReverseIt,
      name: MyApp.ReverseProxy,
      backend: "http://api-server:4000",
      strip_path: "/api"

### WebSocket Proxying

    # Proxy WebSocket connections
    forward "/ws", ReverseIt,
      name: MyApp.ReverseProxy,
      backend: "ws://websocket-server:4000"

### Custom Timeouts and Protocols

    # Configure timeouts and HTTP protocols
    forward "/", ReverseIt,
      name: MyApp.ReverseProxy,
      backend: "https://backend:443",
      upstream_idle_timeout: 60_000,
      protocols: [:http1, :http2]

### Customizing Requests and Responses

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

    defmodule MyApp.APIProxy do
      @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

# `buffered_response`

```elixir
@type buffered_response() :: %{
  status: pos_integer(),
  headers: [{String.t(), String.t()}],
  body: binary()
}
```

A complete, unsent response returned by `request/3`.

# `child_spec`

Child spec for starting ReverseIt with a Finch connection pool.

Add this to your application's supervision tree:

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

## Options

  * `:name` (required) - Name for the Finch pool
  * `:pool_size` - Max connections per backend (default: 50)
  * `:pool_count` - Number of connection pools (default: 1)
  * `:connect_timeout` - Backend connection timeout in ms (default: 5_000)
  * `:upstream_send_timeout` - Socket write timeout for pooled connections in ms (default: 55_000)
  * `:conn_max_idle_time` - Idle timeout for pooled backend HTTP/1 connections (default: 90_000)
  * `:protocols` - Upstream protocols for pooled Finch requests (default: [:http1])
  * `:inet6` - Try IPv6 before IPv4 for pooled connections (default: false)

# `request`

```elixir
@spec request(Plug.Conn.t(), ReverseIt.Config.t(), keyword()) ::
  {:ok, Plug.Conn.t(), term()}
  | {:buffered, buffered_response(), Plug.Conn.t(), term()}
  | {:error, term(), Plug.Conn.t(), term()}
```

Proxies HTTP with caller-owned response handling, without halting the conn.

Returns `{:ok, conn, handler_state}` for a sent/streamed response,
`{:buffered, %{status: status, headers: headers, body: body}, conn, handler_state}`
for an unsent buffered response, or `{:error, reason, conn, handler_state}`
for a failure before commitment. Returned errors are not logged; the caller
owns logging. After commitment, failures exit the request process to abort
the downstream response. With a response handler, `terminate/2` receives the
failure before that exit and the caller owns logging; otherwise ReverseIt
logs the failure. WebSocket upgrades are rejected.

The optional third argument accepts per-request options:

  * `:request_body` - Already-read binary bytes to forward, including `""`.
    `nil` (the default) reads the body from the conn. Body limits still apply.
  * `:response_handler` - `{module, initial_state}` for response processing.
    `nil` (the default) streams the response unchanged.

Initialize the static config once, then pass fresh options for each request.
These options are rejected by `init/1`; handler modules are checked at request
time rather than while the router is compiling. See `ReverseIt.ResponseHandler`
for the callback contract. Buffered/error results leave the response unsent so
the caller can retry or send its own response.
Only replay explicitly supplied bodies; the original conn body is consumed.

# `send_buffered`

```elixir
@spec send_buffered(Plug.Conn.t(), buffered_response()) :: Plug.Conn.t()
```

Sends a buffered response returned by `request/3`, without halting the conn.

Replaces existing response headers, including Plug's default Cache-Control,
while preserving repeated upstream headers such as Set-Cookie. Registered
`Plug.Conn.register_before_send/2` callbacks still run. Headers set by earlier
plugs, such as request IDs and CORS headers, are replaced too; restore those
headers in a `Plug.Conn.register_before_send/2` callback to retain them.

If you change the buffered body, update its representation headers (such as
Content-Length and Content-Encoding) before sending it.

---

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