- PagerDuty /
- Engineering Blog /
- Creating an HTTP Server Using Pure OTP
Engineering Blog
Creating an HTTP Server Using Pure OTP
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:
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):
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.