Rack: Part I

Austin Miller
9 min readOct 29, 2020
Photo by Francesco Ungaro on Unsplash, Rack Lobster

Introduction

This is the first in a series of two articles. You can find the second article here or at the bottom of the page.

These two articles focus on the Rack RubyGem — what it is, where it sits in server-side development, what it means for an application to be Rack-based, and what Rack accomplishes for developers and the frameworks they work with behind the scenes. We’ll dig into a little Rack source code and send some really simple responses back to our browser to demonstrate the material.

The series has been a learning exercise for me, and I hope it serves as a deeper look into Rack for anyone who’s interested. Please do not hesitate to leave a comment here on Medium or reach out to me in Slack on Launch School. I would love to make these articles as accurate and complete as possible, so please don’t hesitate to leave any feedback.

You may find this material most useful if you have a broad understanding of network communication, procedural and object-oriented programming, and Ruby. You may also find this material useful if you have some experience with a framework like Sinatra or Ruby on Rails, and want to learn about how Rack functions at a lower level. With no further ado, let’s get started!

What is Rack?

Rack is a RubyGem that provides an interface for your Ruby web application and the application server you’re using. Rack-based applications are those that follow a set of specifications established by Rack that allow Rack to work with the application.

Rack is a generic interface — it facilitates smooth communication between your Rack-based Ruby app (whether it’s a Sinatra app, a Rails app, an app written using a different framework, or an app written in plain Ruby) and the application server you’re working with (whether you’re using WEBrick, Puma, Passenger, Unicorn, Thin, etc.).

The diagram below is a high level look at what happens server-side after a user initiates a request by typing in a URL or clicking a link in a browser.

The Rack RubyGem serves as an interface between the application and the application server during the request-response cycle.

Rack sits between the application server and the application. In the client-server direction, Rack takes plain text HTTP requests and formats them into Ruby data structures that can be more easily used by your Ruby program or framework. In the server-client direction, Rack then takes the return value of your Ruby program and formats it into HTTP-compliant text to be sent back to the client as an HTTP response.

Rack allows your application to focus on core business logic — making server-side changes and generating dynamic content. It takes care of lower level tasks like handling HTTP requests and responses.

We can zoom in to the exchange of information between the application and the application server to show Rack’s role as a generic interface.

Rack offers support for many servers, and many applications can be Rack-based.

Rack provides a standardized way for an application and servers to communicate. A Rack-based application could use WEBrick or Puma — without having to make changes to a configuration file.

What makes an application Rack-based?

An application is considered Rack-based if it adheres to Rack specifications. Namely, an application must define a call method. This method is the heart of a Rack-based application. There are two requirements for the method.

  1. call must be defined to take one parameter, often called env.
A Rack app must define a method named call that takes one argument.

2. call must return an Array object with three items. The three items are a) a String or Integer object that represents a status code, b) a Hash object that holds key-value pairs of content headers and their values, and c) an object that responds to Enumerable#each and holds the body of the response.

This constitutes a minimum viable Rack application

When call is actually invoked and passed an argument, env will be bound to a Hash object with HTTP headers and other environment variables that relate to the incoming HTTP request. In short, env is a Hash with all the information about an incoming request that you — or the framework that you’re writing your application in — can use. We’ll take a closer look at env shortly.

The return value of the call method is used to format an HTTP response that will eventually be sent back to the client.

Finally, Rack needs to know what to run and how to run it. This can be achieved by using a rackup config.ru file or by requiring ‘rack' and invoking Rack::Handler::WEBrick.run in the file like we do below.

Let’s examine this canonical example of a simple Rack application.

require 'rack'class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, ["hello world"]]
end
end
Rack::Handler::WEBrick.run MyApp.new

If you’re following along in a code editor, you can copy and paste this code and save your file as myapp.rb. If you’re not sure if you have the rack gem installed on your machine, you can run gem install rack in the command line. (If you’re not sure you have webrick, you can do the same for that gem). Then you can run ruby myapp.rb in the command line.

This starts a server on port 8080, listening for incoming requests from a client. To send a request, open a browser and type in http://www.localhost:8080. You should see hello world in that tab.

To quickly demonstrate another example that returns HTML that will be interpreted by the client, below is an example where we change the "Content-Type" to "text/html", which tells the browser to interpret the body as HTML rather than plain text.

require 'rack'class MyApp
def call(env)
body = "<h2>Hello in Style!</h2>"
['200', { "Content-Type" => "text/html" }, [body]]
end
end
Rack::Handler::WEBrick.run MyApp.new

This time, we initialize a local variable body, assign it to a String object that can be interpreted by the browser as HTML, specify this new type of content to the browser, and return body as the third element in our Array, which will be displayed in the browser.

What is env?

Let’s take a look at the env request hash by including it as the body of our response to the browser.

require 'rack'class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, [env.to_s]]
end
end
Rack::Handler::WEBrick.run MyApp.new

If you run ruby myapp.rb in the command line and visit www.localhost:8080, you’ll see the contents of env that are available to your Rack-based application. You’ll see information like "PATH_INFO" => "/", "QUERY_STRING" => "", "REQUEST_METHOD" => "GET", and "HTTP_CACHE_CONTROL" => "max-age=0". You’ll also see information that Rack appends to the HTTP request, like "rack.version" => [1, 3], "rack.multithread" => true, and "rack.url_scheme" => "http".

env is a Hash with key-value pairs of information that comes from the HTTP request, the server, and Rack itself. You can read more about it and see an example here. Rack, Rack middleware, and Rack-based frameworks use the information in this Hash to execute logic. The "PATH_INFO" and "REQUEST_METHOD" are generally the most useful pieces of information for you as the developer. "PATH_INFO" shows the path added to the URL after the hostname, which you (or frameworks you use) can use to direct users to certain pages and execute particular logic. You can use the "REQUEST_METHOD" to understand if the user is requesting a server-side change or simply requesting a resource.

Next level of abstraction, please

Let’s circle back to the first iteration of our simple Rack-based application.

require 'rack'class MyApp
def call(env)
['200', { "Content-Type" => "text/plain" }, ["hello world"]]
end
end
Rack::Handler::WEBrick.run MyApp.new

What’s happening in this code snippet? We’re requiring the Rack library. This gives us access to the Rack::Handler::WEBrick::run method that we invoke on the final line of code, as well as all the other classes and modules implemented as part of the Rack gem.

On the final line, we’re calling ::new on our Rack-based MyApp class we define above. We then invoke Rack::Handler::WEBrick::run and pass in the instance of our class. What’s happening with all of those constants strung together?

They’re all different classes and modules included in the Rack library. You can peruse the Rack source code here. If we navigate into the /lib/rack/ subdirectory of the Rack Github repo, we’ll see a Handler subdirectory. Let’s check that out, as that’s the second constant used to access this ::run method.

Okay … because we’re invoking Rack::Handler::WEBrick::run, let’s jump into the webrick.rb file.

This file includes webrick and stringio, then carries out a little monkey patching to accomplish out-of-scope tasks, then opens a module Rack, a nested module Rack::Handler, and a nested class Rack::Handler::WEBrick (which inherits from a subclass of the WEBrick class that’s been included and monkey patched up above).

This matches the chained modules and classes prepended to the ::run class method we invoke in our myapp.rb file! Great — now we can search for our ::run method.

Rack source code here

We don’t have to search very far. ::run is defined on the first line of the nested Rack::Handler::WEBrick class. It looks like ::run is going to take an app and an optional options Hash object.

This optional Hash allows us to specify a hostname and a port other than the default development values of localhost and 8080. On lines 27-28, we’re accessing ENV (unrelated to env passed to call) to check the environment we’re running this application in to decide what the hostname should be. On lines 30-36, we set a hostname, a port, and lay the groundwork for WEBrick to use HTTPS if that’s been specified in the application.

On line 38, we initialize @server and assign it to the value of calling ::new on the WEBrick::HTTPServer class. While out of the scope of this article, this class opens and closes a socket and handles any connection errors, among other items. Lines 38-42 use the classes and modules defined in the WEBrick gem to do what WEBrick does best — set up an HTTP server.

In our search for Rack::Handler::WEBrick::run, we discovered how it’s implemented, how Rack sets up a hostname and port, enables support for HTTPS using webrick/https , and even a little about how WEBrick works. We did not look into Rack handlers, though if we did, we’d see that Rack handlers “connect web servers with Rack,” along with a short list of supported web servers.

Part 1: Summary

To summarize our discussion so far, we’ve talked about how Rack is a RubyGem that operates between a Ruby application and an application server. We’ve talked about Rack-based applications and the required call method, the env hash it takes and the 3-item Array it must return. We’ve also managed to send a response from our application to the browser.

Finally, we were able to peer down another level of abstraction into Rack source code to start getting an idea about what we’re doing — and really, what Rack, WEBrick, and other servers that Rack can handle are accomplishing behind the scenes. But — Rack does so much more. In the second article, we’ll examine how we can use Rack to stack multiple modular applications on top of each other.

Continue reading: Rack: Part II

--

--

Austin Miller

Research analyst turned aspiring web developer. Learning the fundamentals with Launch School. Lives in Denver, CO.