README.md

# ReverseProxyPlug

A reverse proxy plug for proxying a request to another URL using [HTTPoison](https://github.com/edgurgel/httpoison).
Perfect when you need to transparently proxy requests to another service but
also need to have full programmatic control over the outgoing requests.

This project grew out of a fork of
[elixir-reverse-proxy](https://github.com/slogsdon/elixir-reverse-proxy).
Advantages over the original include more flexible upstreams, zero-delay
chunked transfer encoding support, HTTP2 support with Cowboy 2 and focus on
being a composable Plug instead of providing a standalone reverse proxy
application.

## Installation

Add `reverse_proxy_plug` to your list of dependencies in `mix.exs`:
```elixir
def deps do
  [
    {:reverse_proxy_plug, "~> 1.3.1"}
  ]
end
```

## Usage

The plug works best when used with
[`Plug.Router.forward/2`](https://hexdocs.pm/plug/Plug.Router.html#forward/2).
Drop this line into your Plug router:

```elixir
forward("/foo", to: ReverseProxyPlug, upstream: "//example.com/bar")
```

Now all requests matching `/foo` will be proxied to the upstream. For
example, a request to `/foo/baz` made over HTTP will result in a request to
`http://example.com/bar/baz`.

You can also specify the scheme or choose a port:
```elixir
forward("/foo", to: ReverseProxyPlug, upstream: "https://example.com:4200/bar")
```

The `:upstream` option should be a well formed URI parseable by [`URI.parse/1`](https://hexdocs.pm/elixir/URI.html#parse/1),
or a zero-arity function which returns one. If it is a function, it will be
evaluated for every request.

### Usage in Phoenix
The Phoenix default autogenerated project assumes that you'll want to
parse all request bodies coming to your Phoenix server and puts `Plug.Parsers`
directly in your `endpoint.ex`. If you're using something like ReverseProxyPlug,
this is likely not what you want — in this case you'll want to move Plug.Parsers
out of your endpoint and into specific router pipelines or routes themselves.

Or you can extract the raw request body with a
[custom body reader](https://hexdocs.pm/plug/1.6.0/Plug.Parsers.html#module-custom-body-reader)
in your `endpoint.ex`:
```elixir
plug Plug.Parsers,
  body_reader: {CacheBodyReader, :read_body, []},
  # ...
```
and store it in the `Conn` struct with custom plug `cache_body_reader.ex`:
```elixir
defmodule CacheBodyReader do
  @moduledoc """
  Inspired by https://hexdocs.pm/plug/1.6.0/Plug.Parsers.html#module-custom-body-reader
  """

  alias Plug.Conn

  @doc """
  Read the raw body and store it for later use in the connection.
  It ignores the updated connection returned by `Plug.Conn.read_body/2` to not break CSRF.
  """
  @spec read_body(Conn.t(), Plug.opts()) :: {:ok, String.t(), Conn.t()}
  def read_body(%Conn{request_path: "/api/" <> _} = conn, opts) do
    {:ok, body, _conn} = Conn.read_body(conn, opts)
    conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
    {:ok, body, conn}
  end

  def read_body(conn, _opts), do: {:ok, nil, conn}
end
```
which then allows you to use the [Phoenix.Router.forward/4](https://hexdocs.pm/phoenix/Phoenix.Router.html#forward/4)
in the `router.ex`:
```elixir
  scope "/api" do
    pipe_through :api

    forward "/foo", ReverseProxyPlug,
      upstream: &Settings.foo_url/0,
      error_callback: &__MODULE__.log_reverse_proxy_error/1

    def log_reverse_proxy_error(error) do
      Logger.warn("ReverseProxyPlug network error: #{inspect(error)}")
    end
  end
```

### Modifying the client request body
You can modify various aspects of the client request by simply modifying the
`Conn` struct. In case you want to modify the request body, fetch it using
`Conn.read_body/2`, make your changes, and leave it under
`Conn.assigns[:raw_body]`. ReverseProxyPlug will use that as the request body.
In case a custom raw body is not present, ReverseProxyPlug will fetch it from
the `Conn` struct directly.

### Response mode

`ReverseProxyPlug` supports two response modes:

- `:stream` (default) - The response from the plug will always be chunk
encoded. If the upstream server sends a chunked response, ReverseProxyPlug
will pass chunks to the clients as soon as they arrive, resulting in zero
delay.

- `:buffer` - The plug will wait until the whole response is received from
the upstream server, at which point it will send it to the client using
`Conn.send_resp`. This allows for processing the response before sending it
back using `Conn.register_before_send`.

You can choose the response mode by passing a `:response_mode` option:
```elixir
forward("/foo", to: ReverseProxyPlug, response_mode: :buffer, upstream: "//example.com/bar")
```

### Connection errors

`ReverseProxyPlug` will automatically respond with 502 Bad Gateway in case of
network error. To inspect the HTTPoison error that caused the response, you
can pass an `:error_callback` option.

```elixir
plug(ReverseProxyPlug,
  upstream: "example.com",
  error_callback: fn error -> Logger.error("Network error: #{inspect(error)}") end
)
```

You can also provide a MFA (module, function, arguments) tuple, to which the
error will be inserted as the last argument:

```elixir
plug(ReverseProxyPlug,
  upstream: "example.com",
  error_callback: {MyErrorHandler, :handle_proxy_error, ["example.com"]}
)
```

### Callbacks for responses in streaming mode

In order to add special handling for responses with particular statuses instead
of passing them on to the client as usual, provide the `:status_callbacks`
option with a map from status code to handler:

```elixir
plug(ReverseProxyPlug,
  upstream: "example.com",
  status_callbacks: %{404 => &handle_404/2}
)
```

The handler is called as soon as an `HTTPoison.AsyncStatus` message with the
given status is received, taking the `Plug.Conn` and the options given to
`ReverseProxyPlug`. It must then consume all the remaining incoming HTTPoison
asynchronous response parts, respond to the client and return the `Plug.Conn`.

`:status_callbacks` must only be given when `:response_mode` is `:stream`,
which is the default.

## License

ReverseProxyPlug is released under the MIT License.