Webmachine in Elixir Tutorial, Part 1
by Sean Cribbs
Webmachine is an Erlang project I’ve known and used for years, especially thanks to my experience at Basho. One of the things that made me proud to use it was its opinionated nature about HTTP, namely, that in most cases you don’t have to manually set status codes or supply response headers. Instead, you supply predicate functions that answer very specific questions about your HTTP resource, and the Webmachine FSM calls those to determine how to respond to each request. This leads to a very declarative style, which is a great fit for functional programming, and it becomes simple to extend your resource with more capabilities.
In contrast, the trend in the Elixir community is to use Plug, often in conjunction with Phoenix. Plug is very much in the style of Rack and WSGI which came before it, and I’ve ranted about the problems with those before (and will not repeat here).
That said, someone has
“ported” Webmachine to Elixir
and provided a complicated macro-based DSL with which to declare
resources. When I
ported Webmachine to Ruby,
I eschewed DSLs for a simple inherit-and-override model which was
incredibly successful and easy to understand, in my opinion, not to
mention faster and more efficient than hacks using instance_eval
.
In this post, I hope to convince you that Elixir’s simplicity and direct interaction with Erlang libraries makes it possible to use Webmachine in your Elixir project, without a rewrite or translation layer.
Before I dive into it, you should be aware some caveats.
Most Elixir projects use Cowboy or Elli for the
webserver. Unfortunately, Webmachine is only compatible with
mochiweb
; although previously efforts have been made to connect Yaws
and Cowboy, those have never completed.
There’s some cruft in Webmachine, simply because it was started before 2009. I hope it won’t be too onerous to work around.
At the time of writing, I’m running Elixir 1.2.3 atop Erlang/OTP 18.2.1 on Mac OS/X, both installed via homebrew.
I’ll assume you know a little bit about Elixir syntax and idioms, and won’t add too much discussion of the basics.
Getting Started
Let’s start by making a new project with mix
, and then we’ll add
Webmachine and a basic resource that returns some HTML. I’ll be
roughly following the
tutorial that
Chris Meiklejohn and I gave to LambdaJam in 2013, which is a very
basic Twitter clone called “Tweeter”.
$ mix new tweeter --sup
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/tweeter.ex
* creating test
* creating test/test_helper.exs
* creating test/tweeter_test.exs
$ cd tweeter
Now we can add Webmachine to the project and compile. First, edit
mix.exs
to add Webmachine.
defmodule Tweeter.Mixfile do
use Mix.Project
def project do
[app: :tweeter,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
def application do
# Add :webmachine here
[applications: [:logger, :webmachine],
mod: {Tweeter, []}]
end
defp deps do
# Add webmachine dep here
[{:webmachine,
git: "https://github.com/webmachine/webmachine.git",
branch: "master"}]
end
end
Fetch the dependencies and compile!
$ mix deps.get
* Getting webmachine (https://github.com/webmachine/webmachine.git)
Cloning into '/Users/scribb201/dev/tweeter/deps/webmachine'...
remote: Counting objects: 3172, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 3172 (delta 1), reused 0 (delta 0), pack-reused 3168
Receiving objects: 100% (3172/3172), 2.89 MiB | 146.00 KiB/s, done.
Resolving deltas: 100% (1767/1767), done.
Checking connectivity... done.
A new Hex version is available (v0.11.1), please update with `mix local.hex`
Running dependency resolution
Dependency resolution completed successfully
mochiweb: v2.12.2
* Getting mochiweb (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/mochiweb-2.12.2.tar)
Fetched package
Unpacked package tarball (/Users/scribb201/.hex/packages/mochiweb-2.12.2.tar)
$ mix compile
==> mochiweb (compile)
Compiled src/reloader.erl
Compiled src/mochiweb_websocket.erl
Compiled src/mochiweb_util.erl
Compiled src/mochiweb_socket.erl
Compiled src/mochiweb_session.erl
Compiled src/mochiweb_response.erl
Compiled src/mochiweb_socket_server.erl
Compiled src/mochiweb_multipart.erl
Compiled src/mochiweb_io.erl
Compiled src/mochiweb_mime.erl
Compiled src/mochiweb_http.erl
Compiled src/mochiweb_headers.erl
Compiled src/mochiweb_request.erl
Compiled src/mochiweb_echo.erl
Compiled src/mochiweb_cover.erl
Compiled src/mochiweb_cookies.erl
Compiled src/mochiweb_base64url.erl
Compiled src/mochiweb_acceptor.erl
Compiled src/mochiweb.erl
Compiled src/mochiutf8.erl
Compiled src/mochiweb_html.erl
Compiled src/mochitemp.erl
Compiled src/mochilogfile2.erl
Compiled src/mochilists.erl
Compiled src/mochinum.erl
Compiled src/mochijson.erl
Compiled src/mochihex.erl
Compiled src/mochiglobal.erl
Compiled src/mochijson2.erl
Compiled src/mochifmt_std.erl
Compiled src/mochifmt_records.erl
Compiled src/mochifmt.erl
Compiled src/mochiweb_charref.erl
==> webmachine (compile)
Compiled src/wrq.erl
Compiled src/wmtrace_resource.erl
Compiled src/webmachine_util.erl
Compiled src/webmachine_sup.erl
Compiled src/webmachine_router.erl
Compiled src/webmachine_perf_log_handler.erl
Compiled src/webmachine_resource.erl
Compiled src/webmachine_mochiweb.erl
Compiled src/webmachine_multipart.erl
Compiled src/webmachine_logger_watcher_sup.erl
Compiled src/webmachine_logger_watcher.erl
Compiled src/webmachine_log.erl
Compiled src/webmachine_error_log_handler.erl
Compiled src/webmachine_error.erl
Compiled src/webmachine_error_handler.erl
Compiled src/webmachine_deps.erl
Compiled src/webmachine_dispatcher.erl
Compiled src/webmachine_app.erl
Compiled src/webmachine_access_log_handler.erl
Compiled src/webmachine.erl
Compiled src/webmachine_request.erl
Compiled src/webmachine_decision_core.erl
Compiled lib/tweeter.ex
Generated tweeter app
Consolidated List.Chars
Consolidated String.Chars
Consolidated Collectable
Consolidated Enumerable
Consolidated IEx.Info
Consolidated Inspect
My First Resource
Now that we’ve verified we can build everything, let’s hook up the
webserver and a basic resource to serve some content. To do that, we
need to start the server as a child of our application
supervisor. Luckily, we generated a supervisor when we ran mix
new
. Edit lib/tweeter.ex
to look like this:
defmodule Tweeter do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
# Some configuration that Webmachine needs
web_config = [ip: {127, 0, 0, 1},
port: 8080,
dispatch: []]
# Add the webmachine+mochiweb listener
children = [
worker(:webmachine_mochiweb, [web_config],
function: :start,
modules: [:mochiweb_socket_server])
]
opts = [strategy: :one_for_one, name: Tweeter.Supervisor]
Supervisor.start_link(children, opts)
end
end
Now run the app and go to http://127.0.0.1:8080/.
$ mix run --no-halt
You should get a very “Web 1.0” 404 page. Hit Ctrl-C a ENTER
in the
shell to exit the Mix process. Let’s add a “Hello, World” resource so
we can get something other than the 404 page. Following Elixir
conventions, we’ll build out our source tree to separate concerns.
$ mkdir -p lib/tweeter/resources
$ $EDITOR lib/tweeter/resources/hello.ex
Below is what we’ll put into hello.ex
, along with a little
explanation of why.
defmodule Tweeter.Resources.Hello do
# Initializes the resource's state, which is nothing right now
def init(_) do
{:ok, nil}
end
# Required callback, but almost never overridden! Most people use
# service_available/2 instead. In Erlang, we'd do this to avoid it:
#
# -include_lib("webmachine/include/webmachine.hrl").
#
# Maybe later we'll make a macro that automatically includes it.
def ping(req_data, state) do
{:pong, req_data, state}
end
# Default body callback, producing HTML.
def to_html(req_data, state) do
{"<html><body>Hello, World!</body></html>", req_data, state}
end
end
That isn’t too bad, right? You do have some strange 3-tuple return values. What are they?
The 3-tuple return values thread the request/response data and the
resource state through the functions, while the first element of each
is the callback-specific return value. So, for ping/2
we need to
return :pong
for success, and to_html/2
needs to return the
response body. This pattern will repeat as we add more functionality
to our resources.
Now let’s hook it up to the webserver in tweeter.ex
:
# Some configuration that Webmachine needs
web_config = [ip: {127, 0, 0, 1},
port: 8080,
dispatch: [
{[], Tweeter.Resources.Hello, []}
]]
What we’ve modified here is the “dispatch list”, or “routes” as they
are called in other frameworks. The []
in the first position says
we’ll match the root URL, or /
. The second position is the module
name of our resource. The third position is any additional arguments
to initialize our resource (passed to init/1
on startup). Now we can
try it out! Run mix run --no-halt
again and refresh your
browser. You should see “Hello, World!” on the screen.
Up next
In the next installment, we’ll learn some more parts of Webmachine resources to serve up some static content.