Webmachine in Elixir Tutorial, Part 2
by Sean Cribbs
Last time, we got our project set up and serving some simple dynamic content. In this installment, we’ll show how to serve static files via Webmachine so we can discuss lots of its best features.
Serving static files
Most times you would let a web-server like Apache or nginx serve your
static files, but for our tutorial it’s nice to serve our content
directly via Webmachine. By doing so, we can demonstrate several
important features related to the dispatcher, content-negotiation, and
conditional requests. Basically, everything you’d expect out of a
well-configured web-server but in a resource module! First make a
priv
directory (where OTP apps store non-code files) and we’ll put
our design assets in there. For now, we’ll just copy files from the
webmachine-tutorial
repo.
Let’s make a new resource module called Tweeter.Resources.Assets
and
fill out the boilerplate. For our resource state, we’ll use a map this
time, but we’ll probably change it to a struct later.
Now we need to think about a few things, namely, how to determine
which file is being requested, what media type it is, and then how to
read it from the filesystem out to the client. Let’s start from the
end, assuming we’ve already determined the correct file to read. We’ll
make a body-producing function that simply reads the file and sends it
to the client. This is not the most efficient way – sendfile()
or
other streaming would be better – but we are serving small files so
it won’t be too bad.
That was easy! Continuing backwards through our list, let’s
determine the media type of the file and point it at our
body-producing function using the content_types_provided
Webmachine callback. This callback tells Webmachine what media
types you provide, and what to call to produce each one. Since ours
is just reading a file from the filesystem, we’ll call
produce_resource
, but vary the type it produces.
This is the first time we’ve used a Webmachine library function in
a resource. :wrq.disp_path
gives us the portion of the path that
the dispatcher matched against. So at the root URL, this will be
the empty string, otherwise, it’ll be a partial path to some file,
like css/master.css
. Then :webmachine_util.guess_mime
is used
to guess what a proper media type will be. For fun, let’s try that
function from the shell via iex -S mix
.
Now that we have a body producing function, and the correct MIME
type, let’s find the file on the filesystem, via one of the most
important callbacks resource_exists
. Obviously, if the file
doesn’t exist in our static assets, we should return a 404 Not
Found
, and this is also a perfect place to populate the state with
an absolute path to the requested file.
Before we move on, there’s some repeated functionality with
content_types_provided
, and we have a minor bug too – at the
root path we want to serve index.html
. Let’s extract that shared
functionality into a new function.
To get this resource to actually serve content, we now need to hook
it up to the dispatcher. Let’s edit tweeter.ex
again, replacing
our Hello
resource with Assets
.
Note the special path segment :*
. This tells the Webmachine
dispatcher to match any number of trailing path
segments. Kill/restart your mix
process and refresh the page!
Reducing waste
This strategy of reading a file from disk and sending it to the client is as old as the web itself, but there’s much more we can do! HTTP includes caching in the protocol, and it’d be pretty inefficient for a client to fetch unchanged design assets every time they refresh the page.
Let’s add some simple validation caching to our assets resource. We
can start by using the last_modified
callback.
This is pretty simple: we read the file statistics, pulling out the
mtime
field which represents when it was last modified. We can
use the File.stat!
function instead of its safe equivalent
because of the flow of Webmachine’s decision graph. That is, we
know that last_modified
will not be called if resource_exists
returns false
.
We can go even further by using entity tags, or “ETag” for
short. These are usually a hash string of various aspects of the
file’s metadata. Since we might be doing that File.stat!
call in
multiple places, let’s put it in resource_exists
while we’re at
it and save the result.
We use the built-in :erlang.phash2
function to compute the ETag,
but you should probably use a better hash in other resources.
Finally, I noticed that our CSS and HTML, although small, are still
multiple kilobytes. We can reduce the transmission time
significantly through compression, using the encodings_provided
callback. Somewhat similar to content_types_provided
, it returns
a list of pairs, where the first is an encoding and the second is a
fn
that performs the encoding.
Note that this is a case again where Webmachine requires character
lists and not binaries (single-quoted strings). Now that our
compression is in place, I see the index.html
file going from
~1KB to 560B, and the CSS file from 6.7KB to ~1KB. Nice bandwidth
savings!
Up next
In the next installment, we’ll learn start serving some dynamically-generated content from a resource.