# ReverseProxyPlug

A reverse proxy plug for proxying a request to another URL using [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
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

## Installation

Add `reverse_proxy_plug` to your list of dependencies in `mix.exs`:
def deps do
    {:reverse_proxy_plug, "~> 1.3.3"}

## Usage

The plug works best when used with
Drop this line into your Plug router:

forward("/foo", to: ReverseProxyPlug, upstream: "//")

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

You can also specify the scheme or choose a port:
forward("/foo", to: ReverseProxyPlug, upstream: "")

The `:upstream` option should be a well formed URI parseable by [`URI.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](
in your `endpoint.ex`:
plug Plug.Parsers,
  body_reader: {CacheBodyReader, :read_body, []},
  # ...
and store it in the `Conn` struct with custom plug `cache_body_reader.ex`:
defmodule CacheBodyReader do
  @moduledoc """
  Inspired by

  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}

  def read_body(conn, _opts), do: Conn.read_body(conn, opts)
which then allows you to use the [Phoenix.Router.forward/4](
in the `router.ex`:
  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)}")

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

- `: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:
forward("/foo", to: ReverseProxyPlug, response_mode: :buffer, upstream: "//")

### Custom HTTP methods

Only standard HTTP methods in "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS",
"TRACE" and "PATCH" will be forwarded by default. You can specific define other custom
HTTP methods in keyword :custom_http_methods.

forward("/foo", to: ReverseProxyPlug, upstream: "//", custom_http_methods: [:XMETHOD])

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

  upstream: "",
  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:

  upstream: "",
  error_callback: {MyErrorHandler, :handle_proxy_error, [""]}

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

  upstream: "",
  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.