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

" headers . each do | k , v | response << " #{ k } : #{ v } \r

" end response << "Connection: close \r

" socket . print response socket . print " \r

" 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.