An Elixir Plug that targets a specific path
Plug is a cornerstone of Elixir and handling HTTP requests. It’s striking how simple it is to write one:
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:
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:
So, in concrete terms, given that the health check path is /_health_check
:
- If
path_info
matches, respond with a200
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
fromPlug.Router
orPhoenix.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:
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).
Conclusion
All that was done in here was to inspect what’s inside a conn
and 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.