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 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
:
def deps do
[
{:reverse_proxy_plug, "~> 2.1"}
]
end
Then add an http client library, either httpoison or tesla
Also set configration:
config :reverse_proxy_plug, :http_client, ReverseProxyPlug.HTTPClient.Adapters.HTTPoison
# OR
config :reverse_proxy_plug, :http_client, ReverseProxyPlug.HTTPClient.Adapters.Tesla
You can also set the config as a per-plug basis, which will override any global config. Either of those must be set, otherwise the system will attempt to default to the HTTPoison adapter or raise if it's not present.
plug ReverseProxyPlug, client: ReverseProxyPlug.HTTPClient.Adapters.Tesla
Usage
The plug works best when used with
Plug.Router.forward/2
.
Drop this line into your Plug router:
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:
forward("/foo", to: ReverseProxyPlug, upstream: "https://example.com:4200/bar")
The :upstream
option should be a well formed URI parseable by URI.parse/1
,
or a function with zero or one arity which returns one. If it is a function, it will be
evaluated for every request. If the function is arity one, the Conn
struct will be
passed to it, in order to have more flexibility in dynamic routing.
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 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: Conn.read_body(conn, opts)
end
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)}")
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 usingConn.send_resp
. This allows for processing the response before sending it back usingConn.register_before_send
.
You can choose the response mode by passing a :response_mode
option:
forward("/foo", to: ReverseProxyPlug, response_mode: :buffer, upstream: "//example.com/bar")
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: "//example.com/bar", custom_http_methods: [:XMETHOD])
Connection errors
By default, 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.
plug(ReverseProxyPlug,
upstream: "example.com",
error_callback: fn error -> Logger.error("Network error: #{inspect(error)}") end
)
If you wish to handle the response directly, you can provide a function with arity 2 where the connection will be passed as the second argument:
plug(ReverseProxyPlug,
upstream: "example.com",
error_callback: fn error, conn ->
Logger.error("Network error: #{inspect(error)}")
Plug.Conn.send_resp(conn, :internal_server_error, "something went wrong")
end)
)
You can also provide a MFA (module, function, arguments) tuple, to which the error will be inserted as the last argument:
plug(ReverseProxyPlug,
upstream: "example.com",
error_callback: {MyErrorHandler, :handle_proxy_error, ["example.com"]}
)
If the function specified by the MFA tuple supports two additional arguments, the error and connection will inserted as the last two arguments, respectively.
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:
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.
Copyright and License
Copyright (c) 2018 Tallarium Technologies
ReverseProxyPlug is released under the MIT License.