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.

Comments

© 2006-present Sean CribbsGithub PagesTufte CSS