Brandon Rice

Software development with a focus on web technologies.

Make Your Own Rack Server

| Comments

tl;dr here’s a gist with all of the code.

Every web developer who spends a significant amount of time with Ruby inevitably reaches a point when they want to learn more about Rack. Rack is at the heart of the most popular Ruby web frameworks, including Rails and Sinatra. There are tons of resources available for getting started with Rack applications from the ground up, but I found myself curious about the other side of the fence. How do I write a web server that knows how to talk to Rack applications, and can I get Sinatra to serve a minimal app using that server?

I started with the simplest Sinatra application possible.

1
2
3
4
5
6
# my_server is the server I want to write
set :server, :my_server

get '/' do
  'Hello world!'
end

Trying to run the above application will result in an error because Sinatra is asking Rack to use a server called my_server, and Rack doesn’t know about it. So, let’s tell Rack about the new server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'rack'

# Stub out the server we're making
class MyServer
  def initialize(app)
    @app = app
  end

  def start
    # Handle requests
  end
end

module Rack
  module Handler
    class MyServer
      def self.run(app, options = {})
        server = ::MyServer.new(app)
        server.start
      end
    end
  end
end
Rack::Handler.register('my_server', 'Rack::Handler::MyServer')

Telling Rack about a server is as simple as defining a new handler that lets Rack know how to start the server. The handler has a single method, run, which receives the Rack-compliant application to be served, along with an optional hash of server-specific settings. All that’s left to do is actually implement the server, which is the most significant portion of the entire exercise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class MyServer
  STATUS_CODES = {200 => 'OK', 500 => 'Internal Server Error'}

  attr_reader :app, :tcp_server

  def initialize(app)
    @app = app
  end

  def start
    @tcp_server = TCPServer.new('localhost', 8080)

    loop do
      socket   = tcp_server.accept
      request  = socket.gets
      response = ''

      env = new_env(*request.split)
      status, headers, body = app.call(env)

      response << "HTTP/1.1 #{status} #{STATUS_CODES[status]}\r\n"
      headers.each do |k, v|
        response << "#{k}: #{v}\r\n"
      end
      response << "Connection: close\r\n"

      socket.print response
      socket.print "\r\n"

      if body.is_a?(String)
        socket.print body
      else
        body.each do |chunk|
          socket.print chunk
        end
      end

      socket.close
    end
  end

  def new_env(method, location, *args)
    {
      'REQUEST_METHOD'   => method,
      'SCRIPT_NAME'      => '',
      'PATH_INFO'        => location,
      'QUERY_STRING'     => location.split('?').last,
      'SERVER_NAME'      => 'localhost',
      'SERVER_POST'      => '8080',
      'rack.version'     => Rack.version.split('.'),
      'rack.url_scheme'  => 'http',
      'rack.input'       => StringIO.new(''),
      'rack.errors'      => StringIO.new(''),
      'rack.multithread' => false,
      'rack.run_once'    => false
    }
  end
end

If you’ve ever experimented with writing a basic HTTP server, most of this is boilerplate. Loop continually, waiting for TCP connections. When one is received, pass the request through to the Rack application along with all of the necessary environment settings. When the application is done, send the request back to the client along with any headers, and then close the connection. Obviously, this server has some pretty severe limitations and isn’t intended for actual real-world use.

The only Rack-specific code is the hash created in the new_env method. A Rack application is simply an object that responds to one method, call. That method takes a single argument which is a hash describing the current environment. I took some liberties here because I was only interested in getting the most basic application to work, but the Rack specification describes all of the expected environment values in detail. The takeaway is that Rack applications expect an environment hash, and it’s the job of the server to provide that hash its initial state.

That’s literally all there is to standing up a web server that can speak the Rack language. The small Sinatra app from the beginning of this post should now serve up its Hello World page without a problem. The functionality of this web server is obviously quite limited, but it’s enough to get started on the path toward something more robust. The interesting part to me was how easy this was to put together after a little digging through the Rack source. From the perspective of a server, Rack really is designed to get out of your way while providing a very simple interface to the world of Ruby web apps.

If you enjoyed this post, please consider subscribing.

Comments