How To Create a HTTP Server With Elixir

A HTTP server receives a resquest and gives you a response. It happens through a URL plus a port like https://www.wbotelhos.com:443, when you make a request to it, it gives you my blog home page as the response. So, how can I do it using Elixir?

Goal

Create a HTTP Server that listen for a request and returns a response.

The gen_tcp Module

Erlang has a module called gen_tcp that makes the hard work for you. That code can be converted to Elixir using the following rules to convert from one syntax to another:

ErlangElixir
Variablevariable
symbol:symbol
module:module
:.
“charlist”‘charlist’

Create the Project

mix new how_to_create_a_http_server_with_elixir

Create the Listener

The listener is the socket responsible to listen the requests. Here is a module with a method to create it:

defmodule HttpServer do
  def start(port) do
    {:ok, listen_socket} = :gen_tcp.listen(port, [:binary, packet: :raw, active: false, reuseaddr: true])

    IO.puts("Server running on #{port}...\n")

    accept_connection(listen_socket)
  end
end

The :gen_tcp.listen receives the port to be listened and some options:

  • :binary: The packets are received as a binary data;
  • packet: :raw: Receives the entire pure binary with no changes;
  • active: false: Will receive data only when we explicit allow it;
  • reuseaddr: true: Allow reuse the address even if the server crashes;

Then we need to accept the connections.

Accept Connections

Now it’s time to accept connections from clients. For that we use :gen_tcp.accept providing the listen socket that will result in the client socket. At this point the connection will be hanged waiting for any connection:

def accept_connection(listen_socket) do
  IO.puts("Waiting for connection...\n")

  {:ok, client_socket} = :gen_tcp.accept(listen_socket)

  IO.puts("Connection accepted!\n")

  process_request(client_socket)

  accept_connection(listen_socket)
end

Then we process the connection and turns back to accept connection again. Yes, it’s a loop, it’s receives the request, responds and start to accept connection again.

Process Request

Here we need to read the request and write the response. Very simple, right?

def process_request(client_socket) do
  IO.puts("Processing request...\n")

  client_socket
  |> read_request
  |> create_response()
  |> write_response(client_socket)
end

Read the Request

The :get_tcp.recv receives the client socket and since we’re using packet: :raw options we specify that we want read the entiry binary since the first length. I request is returned to be used:

def read_request(client_socket) do
  {:ok, request} = :gen_tcp.recv(client_socket, 0)

  request
end

Create Response

The response will be a valid HTTP response, where the last line, separated by an empty line, is the body:

def create_response(_request) do
  body = "Hello HTTP Server!"

  """
  HTTP/1.1 200 OK\r
  Content-Type: text/html\r
  Content-Length: #{byte_size(body)}\r
  \r
  #{body}
  """
end

Please, ignore the initial condition for now.

Write Response

Then we just send the response back to the client and close this socket, since it’s done:

def write_response(response, client_socket) do
  :ok = :gen_tcp.send(client_socket, response)

  IO.puts("Response:\n\n#{response}\n")

  :gen_tcp.close(client_socket)
end

Testing

Open the iex:

iex -S mix

And then boot up the server:

HttpServer.start(4000)

# Server running on 4000...

# Waiting for connection...

Now on another terminal make the request:

curl http://localhost:4000

# Hello HTTP Server!

On the server terminal you’ll see:

# Connection accepted!

# Processing request...

# Response:

# HTTP/1.1 200 OK
# Content-Type: text/html
# Content-Length: 18

# Hello HTTP Server!

Dealing With Server Crash

If any error happens, the server will crash and stop working. You can test it adding a condition to raise some error:

def create_response(request) do
  if String.match?(request, ~r{GET /error}) do
    raise(request)
  end

  # ...
end

Now call the route /error:

# Terminal 1

curl http://localhost:4000/error

So in another terminal you can try to send a normal request and it won’t work, since the server crashed:

# Terminal 2

curl http://localhost:4000

It happens because the process_request is processing the response on the same process (PID) that the server it self. So if the process method crashes the server will die together. We can solve it creating a new process to deal with the response:

def accept_connection(listen_socket) do
  # ...

  pid = spawn(fn -> process_request(client_socket) end)

  IO.puts("Processing at PID: #{inspect(pid)}\n")

  # ...
end

Now the process_request will happen on a new process so if it crashes the server will continue working. Add the prefix [#{inspect(pid)}] on all IO.puts for you debug the PIDs:

[#PID<0.138.0>] Server running on 4000...

[#PID<0.138.0>] Waiting for connection...

[#PID<0.138.0>] Connection accepted!

Processing at PID: #PID<0.139.0>

[#PID<0.139.0>] Processing request...

[#PID<0.138.0>] Waiting for connection...

[#PID<0.139.0>] Response:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 18

Hello HTTP Server!

The PID 0.138.0 is where the server is running and the 0.139.0 is where the processing response run once.

Dealing With Opened Socket

If you call the error endpoint curl http://localhost:4000/error the server will be kept alive, but the terminal where you run the call will still kept freezed. It happens because the socket wasn’t properly closed.

Here is why:

{:ok, client_socket} = :gen_tcp.accept(listen_socket)

IO.puts("[#{inspect(self())}] Connection accepted!\n") # #PID<0.138.0>

pid = spawn(fn -> process_request(client_socket) end)

IO.puts("Processing at PID: #{inspect(pid)}\n") # #PID<0.139.0>

The client_socket was created on the PID 0.138.0 and was sent to the PID 0.139.0 where the client socket was supose to be closed. It won’t work because the owner of the client socket is the PID 138 so :gen_tcp.close from PID 139 has no effect. Only PID 138 has this power, so we can bind this responsability:

def accept_connection(listen_socket) do
  # ...

  :ok = :gen_tcp.controlling_process(client_socket, pid)

  # ...
end

Now we’re saying: “My current context self() will control the client_socket registered on this pid”. Try to call the endpoint error again and you have the crash followed by the console release:

curl http://localhost:4000/error

# curl: (52) Empty reply from server

Conclusion

It’s very simple create a HTTP Server using Elixir, but it’s just an example for you understand better how the things work. In a real world you should use a battle tested server like Cowboy.

Repository: https://github.com/wbotelhos/how-to-create-a-http-server-with-elixir

Any suggestion? Please, open an issue here.