Using inets and httpd to create a simple HTTP server without adding external dependencies

Recently I needed to add a healthcheck endpoint to an application that was solely responsbile for reading and writing to Kafka. Normally when I am creating an HTTP application I would reach for Cowboy or Phoenix, however, this use case was very simple: I just needed a single endpoint that would return 200 OK once the application was up and running and was healthy.

Before adding a new external library to our application, lets review what is given to us “for free”. It is well known that Erlang/OTP give us the tools to build robust processes, but OTP is a lot more than that. The definition from the Github page:

OTP is a set of Erlang libraries, which consists of the Erlang runtime system, a number of ready-to-use components mainly written in Erlang, and a set of design principles for Erlang programs

It is a set of libraries, the runtime, and a set of design principles. You can see the list of components here.

The library that is going to help us create our simple HTTP endpoint is called inets. It has a few pieces to it, but the one we are interested in today is the web server, known as httpd (it also has stuff like an FTP client and an HTTP client).




These are the steps needed to get a HTTP server working in your application:

Step 1 — start inets

In you mix.exs file, add inets to the extra_applications value so that it will be started when your application starts. You probably already do this for the logger application:

# mix.exs

def application do
  [
    extra_applications: [:logger, :inets],
    mod: {MyModule, []}
  ]
end

Step 2 — start httpd from supervisor

In the supervisor, we need to start httpd and supervise it. Since mine is for a healthcheck, I started it last, and used a rest-for-one supervision strategy, so that the server would always be restarted (and be down) whenever one of the other processes fails.

Here is some of the code:

# lib/my_app/supervisor.ex

def init(:ok) do
  children =
    [
      kafka_producer_spec(),
      kafka_consumer_spec(),
      other_thing_spec(),
      api_spec() # Start the API last
    ]
    end

    Supervisor.init(children, strategy: :rest_for_one)
end

defp api_spec() do
  options = [
    server_name: 'Api',
    server_root: '/tmp',
    document_root: '/tmp',
    port: 3000,
    modules: [Api]
  ]
  args = [:httpd, options]
  worker(:inets, args, function: :start)
end

There is a lot talk about here. The api_spec/0 function is really just returning the args to make the :inets.start/2 function. You can test that seperately in IEx by doing the following:

Starting inets and httpd from IEx

None of the options that we passed were optional, and you can read more about them in the httpd documentation. The cool thing is that httpd by default is a document server, so after you start it in IEx you can open up your browser and look around at what is in your /tmp folder (specified by the document_root option):

The contents of my /tmp folder, served by httpd

Of course, we are not trying to make a document server. That is why we passed in the extra modules option in our application code (we omitted it while testing in IEx). There is a default list of modules that provide extra functionality for the web server, and one of those defaults is the mod_dir which generated the Apache-style directory browser in the previous screenshot.

By passing our own list of a single module, we will remove any of the default functionality (like the directory browser) and will be responsible for implementing all of the functionality we need ourselves. You can also re-use the default modules by adding them back to your list, they are listed here.

Note that all of the options used 'single_quotes' and not "double_quotes"; this is because we are dealing with an Erlang module that is expecting charlists, which are represented with the single quotes in Elixir.

Step 3 — create Api module

Now we need to define the Api module that we are using in the modules option for httpd, it will handle incoming requests to any URI, do the healthcheck or return a 404:

# lib/my_app/api.ex

defmodule Api do
  require Record

  # Wrap the Erlang Record to make the request_uri parameter easier to access
  Record.defrecord :httpd, Record.extract(:mod, from_lib: "inets/include/httpd.hrl")

  def unquote(:do)(data) do
    response =
        case httpd(data, :request_uri) do
        '/' ->
          if is_healthy?() do
            {200, 'I am healthy'}
          else
            {503, 'I am unhealthy'}
          end
        _   -> {404, 'Not found'}
      end

    {:proceed, [response: response]}
  end

  defp is_healthy?() do
    # Checks some of the other processes
  end
end

There are some uncommon things going on here to talk about.

Record is used for interfacing Erlang records, which are kind of like Elixir structs. In our case, you can see the Record that is defined in inets which represents an incoming request to our server here. You also could achieve the same thing by doing regular pattern matching on the nested data value.

def unquote is for defining our callback function. httpd is expecting our module to have a do/1 function, but we cannot define a function like that in the “regular” way because do is already defined in Elixir! Here is an example in Erlang.

Aside from those, the code itself is pretty straight forward; we pattern match on the route, then call our healthcheck function. We then return a tuple indicating that the request may proceed (the alternative is to return a :break tuple).




This is just scratching the service of inets and httpd but it is a good demonstration of how you can check to see what is already inside of OTP before adding a new external dependency to your project!


Want to chat more about Elixir? PagerDuty engineers can usually be found at the San Francisco and Toronto meetups. And if you’re interested in joining PagerDuty, we’re always looking for great talent — check out our careers page for current open roles.