The Plug package is core to virtually every web application built in Elixir. If you’ve used the popular Phoenix Web Framework, you’ve used Plug before. But, let’s go beyond the basics of just letting Phoenix handle everything for us by exploring what Plug is and how we can make our own. Despite it’s really fun name, I’ll try to keep the puns to a minimum.
The Plug Contract
Plug (the package) centers itself around one major API concept. If you take away nothing else from this post, remember this:
A Plug takes a connection and returns a connection.
When you write your own custom plugs (as we will in this post), you’ll see this in action; our plug will take a connection, potentially modify that connection, and then return it. You can almost think of a plug as like a pipe.
Connections with Plug.Conn
Plug (the package) gives us a robust abstraction for working with connections in the form of the %Plugg.Conn{}
struct.
If you’re familiar with Phoenix, this is the same conn
parameter that you work with in any controller and it has fields for anything your can think of.
If we think of these %Plug.Conn{}
structs as connections, your web application is really just passing a bunch of connections around to be modified before responding to them in the appropriate fashion.
But, when we say that these structs are being “passed around,” what do we mean?
Inside a Plug
Plugs (the thing that takes and returns a %Plug.Conn{}
) come in two flavors: function-based and module-based.
Function-based Plugs
Function-based ones are super simple:
def version_plug(conn, _opts) do
conn
end
Here we’re just defining a super simple function called version_plug
that accepts two parameters: conn
(which is a %Plug.Conn{}
struct) and opts
(which are options, generally used in module-based plugs).
Module-based Plugs
Slightly more complicated, but not very different, we have module-based ones. Here’s what our function-based plug would look like as a module-based plug:
defmodule VersionPlug do
def init([]) do
false
end
def call(conn, _opts) do
conn
end
end
Now we have two functions defined in our module, so let’s take a quick look at those.
The first function declared, init/1
, is what defines the options that we’ll pass to call/2
.
The only caveat is that init/1
can be called at compilation time, so you shouldn’t depend on any processes running or ports being listened to.
In this case, we’re not making use of any options, so we’re just returning false
to make that very explicit.
If you look at call/2
and the version_plug/2
function we defined earlier, you’ll see they only differ in name.
This is where the bulk of the work will probably be happening for most plugs.
Both of these examples are super simple and actually don’t do anything because they just return the %Plug.Conn{}
completely unmodified.
Let’s take a look at a commonly used plug that actually does something, Plug.RequestId
, which is packaged with Plug.
Inside a Useful Plug: Plug.RequestId
Plug.RequestId
is still a very simple plug at heart.
It just wants to make sure that every HTTP request coming in is given a unique identifier.
This unique identifier is added to the response headers and added to our Logger
metadata so that we can find out which Logger
messages are coming from the same request.
This is a very common technique that makes analyzing logs for even smaller web applications a lot easier.
Making use of the plug (and any plug, really) is a one-liner wherever you want the plug implemented:
plug Plug.RequestId
In a normal Phoenix app, this would probably be your endpoint or router.
You can see the code behind this plug on github, but I’ll also be copying relevant portions here.
The Skeleton
Plug.RequestId
has the same two functions we outlined before, init/1
and call/2
:
defmodule Plug.RequestId do
def init(opts) do
Keyword.get(opts, :http_header, "x-request-id")
end
def call(conn, req_id_header) do
conn
|> get_request_id(req_id_header)
|> set_request_id(req_id_header)
end
end
There are a few private functions (get_request_id/2
and set_request_id/2
) not shown here.
It’s perfectly safe to assume they do exactly what they say they do; get_request_id/2
gets the request ID for the connection if it has one and set_request_id/2
sets the request ID header if it doesn’t.
More importantly, this is the first time we’re seeing init/1
used in any capacity, so let’s take a moment to examine at that.
Setting options with init/1
Although it’s fine to just use this plug with
plug Plug.RequestId
You could actually pass a list of options to specify the HTTP header to use as your request ID header, like so:
plug Plug.RequestId, http_header: "x-application-request-uuid"
The list of options specified is the argument passed to init/1
and an empty list ([]
) is passed when no options are specified.
Remember that the return value from init/1
is what is passed as the second argument to call/2
, so in this case, we’re grabbing the :http_header
option, if specified, and returning "x-request-id"
by default.
Using options in call/2
Now that we have the name of the header returned from init/1
, we can make use of it in call/2
.
Since we’re given the connection (%Plug.Conn{}
) as the first argument and the name of the HTTP header to use as the second argument, you can see that getting and setting the header is a breeze.
The bulk of the private functions in this module are actually more about concerned about generating and validating request identifiers than they are about setting the header itself, which is a simple function call: Plug.Conn.put_resp_header(conn, header, request_id)
Putting Plugs Together
By now, you’re probably starting to see the power that comes from the simplicity of the contract that Plug follows. You can basically just plug a few of these together and suddenly your application is behaving like you would expect a web application to!
It’s not uncommon to have a whole list of plugs, each doing a small thing to transform the %Plug.Conn{}
as it moves along.
Phoenix provides a nice abstraction to help organize these pipes into pipelines
:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
Even better, since each of these plugs can be kept simple, they’re very easy to test and debug.
In fact, the test suite for Plug.RequestId
is only five tests and can be run asynchronously!
Building Our Own Plug
Let’s build up our VersionPlug
we started earlier so that it actually does something in our application.
Good software starts with a good understanding of the problem and a good specification, so let’s describe exactly what we want our plug to do.
Our Problem
I get bug reports (either manual or automated through a service like Sentry) for a web application and sometimes the bugs are related to an issue in the Elixir/Phoenix API backing the web application. Sentry can capture the version of the web application that the error occurred on, but it doesn’t get any information about the version of the API. As a result, sometimes I’m left combing through commit and deployment logs and matching timestamps and trying to figure out which version of the API was deployed at the time of the error. It would be great if the version was part of the response headers.
Our Solution
Let’s add a response header to all requests that simply describes the version number of the application.
Our Implementation
Since Plug.RequestId
does something very similar, let’s take some help from the code we just analyzed.
We’ll start with almost the same skeleton:
defmodule VersionPlug do
def init(opts) do
end
def call(conn, opts) do
conn
end
end
We want to allow our plug to be configurable and use an arbitrary header, just like Plug.RequestId
allows:
def init(opts) do
Keyword.get(opts, :http_header, "x-application-version")
end
With that, setting the header in call/2
is easy:
def call(conn, version_header) do
version = Application.spec(:my_app)[:vsn]
Plug.Conn.put_resp_header(conn, version_header, version)
end
Make sure to change :my_app
to whatever your application’s name is.
We can add it to our endpoint like so:
plug VersionPlug
Or, with a custom HTTP header:
plug VersionPlug, http_header: "x-version"
That’s it! Now all HTTP responses will have a new HTTP header with the version of the API. Problem solved!
Conclusion
Plug is probably one of my favorite Elixir packages.
I use it a lot in Phoenix projects and I just love how simple and powerful it’s concept can be.
It really mirrors what I love about Elixir and functional programming in general.
I look forward to writing more about Plug in the future and I particularly want to explore the assigns
map in %Plug.Conn{}
structs and some of the cool things you can do with it.
This article came about because of a poll I took on Twitter asking what I should write about next. After this, people seemed very interested in Gettext, a system for internationalization (i18n) and localization (l10n). Although not as many projects are interested in these features out of the gate, many larger projects grow to accomodate internationalization or exist in a space where mulitple languages aren’t an optional feature. Additionally, as the world continues to grow and become more interconnected, it can be argued that the people helping to facilitate the connections (developers like us) have an ethical responsibility to make our services as widely accessible as possible.
If you’d like to participate in future polls about articles I may write, don’t forget to follow me on Twitter!
Comments