I originally published this post on Heetch Engineering and has been ported here for archiving purposes.


 

Plug is a cornerstone of Elixir and handling HTTP requests. It’s striking how simple it is to write one:

defmodule Example.HelloWorldPlug do
  import Plug.Conn

  def init(options), do: options

  def call(conn, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "Hello World!\n")
  end
end

Yet simple does not necessarily mean that it is easy to use, especially when you’re getting started.

For example, if you look at the code above, it’s not obvious how to update it so it only acts on a specific path. That’s precisely what we’ll be looking at here.

A concrete example

An example of such a feature could be a health check handler. It’s an HTTP route (ex: /_health_check) that can be requested to know if a given web application is performing correctly or not.

  • A 200 OK status code means that yes, it’s operating properly.
  • Anything else would mean that there is a problem and this instance of our application should not receive requests anymore.

This way clients have a way to know if it’s running smoothly or not. For example, container orchestration solutions such as Kubernetes and Mesosphere DC/OS use this mechanism to know if it should kill Docker container running the app or not.

We will be using this use case during the rest of this post.

Why not just use Plug.Router?

If all we want is to match on a given path and respond with a specific response, Plug.Router sounds like an immediate solution. But what if we need to share the health check code among Phoenix apps and other Plug-based apps ?

A module using Plug.Router is a plug itself and thus could be shared. But can’t we just write a bare plug instead for the sake of simplicity?

Let’s see where we start if we’re not using Plug.Router to match specific routes. For a module to be a plug, it needs the call/2 callback to be implemented.

call/2 takes two parameters, conn which is a %Plug.Conn{}, representing our connection to the client and opts, a set of options.

Looking at what is inside the %Plug.Conn{} struct, we can see that there are many interesting fields:

# (straight from https://github.com/elixir-plug/plug/blob/578cd973037bd3e8695817a0c4c69cac9d22db6a/lib/plug/conn.ex#L17-L32)

defmodule Plug.Conn do
  @moduledoc """
  (...)
  ## Request fields
  * (...)
  * `host` - the requested host as a binary, example: `"www.example.com"`
  * `method` - the request method as a binary, example: `"GET"
  * `request_path` - the requested path, example: `/trailing/and//double//slashes/`
  * `path_info` - the path split into segments, example: `["hello", "world"]
  * `query_string` - the request query string as a binary, example: `"foo=bar"`
  * (...)
  (...)
  """
end

All these attributes being inside conn, we can pattern match against them. request_path is a good fit but it may contain trailing slashes. Instead, path_info is a list of each segment in the path, so we don’t have to bother with slashes at all, making it a better choice here.

Let’s see how it goes:

defmodule HealthCheck.Plug do
  import Plug.Conn
  @behaviour Plug

  def init(opts), do: opts

  # path_info matches with health check path!
  def call(conn = %{path_info: ["_health_check"]}, _opts) do
    conn
    |> send_resp(200, "ok")
    |> halt()
  end

  # nope, not for us, pass it down the chain.
  def call(conn, _opts), do: conn
end

So, in concrete terms, given that the health check path is /_health_check:

  • If path_infomatches, respond with a 200 status code and halt the plug pipeline so that no other plugs will be called.
  • Otherwise, just pass the request down the chain to other plugs, like plug :match from Plug.Router or Phoenix.Router.

Using our plug with Plug.Router

The HealthCheck.Plug can be used like any other plug but, if we want to use it within a Plug.Router, where it is inserted can change the outcome. Inserting it after plug :dispatch and plug :match will lead to the request being caught by the catch-all route before reaching our plug.

So the right way to use it is as follows:

defmodule MyApp.Router do
  use Plug.Router

  # This plug must be included **before** `plug :dispatch` and `plug :match`.
  # Otherwise, the reponse will already be sent before it reaches the HealthCheck Plug.
  plug HealthCheck.Plug

  plug :match
  plug :dispatch

  match _ do
    send_resp(conn, 404, "not found")
  end
end

Using our plug with Phoenix

Phoenix works similarly. The plug needs to be inserted before any routing happens. Without diving into details, Phoenix implements its HTTP handler in two components, the endpoint which is a top-level plug and the router, where routes are defined.

So here, it’s required to insert our Plug in the endpoint, before inserting the router (exactly like we’ve seen previously).

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # This plug must absolutely be included **before** `plug MyAppWeb.Router`
  plug HealthCheck.Plug
  plug MyAppWeb.Router
end

Conclusion

All that was done in here was to inspect what’s inside a connand pattern match against it. Very often, pattern matching is itself powerful enough to solve most problems.

In the end and like in other functional languages, looking at what structures are being carried around is really helpful and provides a lot of insight. It should be one of the first things to inspect when exploring code.