Many web developers work at the highest level of abstraction when we code. And sometimes it’s easy to take things for granted. Especially when we are using Rails.
Have you ever delved into how the request / response cycle works in Rails? Recently I realized that I knew almost nothing about how Rack or middleware works – so I spent a little bit of time figuring it out. Here are my findings.
What is Rack?
Did you know that Rails is a Rack application? So is Sinatra. What is Rack? I’m glad you asked. Rack is a Ruby package that provides an easy to use interface between a web server and web framework.
You can quickly create simple web applications with just Rack.
To start, you need an object that responds to a call, uses an Environment Hash and returns an array with the HTTP response code
, header
and response body
. Once you’ve written the server code, all you have to do is launch it using a Ruby server such as Rack::Handler::WEBrick
or put it in the config.ru
file and run it from the command line with rackup config.ru
.
Ok, great. But what does Rack actually do?
How does Rack work?
Rack is really just a way for a developer to create a server application while avoiding pre-compiled code to support different Ruby web servers. If you’ve written some code that meets Rack’s specifications, you can upload it to a Ruby server like WEBrick, Mongrel, or Thin – and you’ll be more than willing to accept requests and respond to them.
There are several methods you should know which are provided for you. You can call them directly from within your config.ru
file.
trembling
Take an application – the call response object – as an argument. The following code snippet from the Rack website shows this way:
1 2 | run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack'd']] } |
map
Get a string specifying the path to be processed and a block containing the Rack application code to be run when a request for that path is received.
Here is an example:
1 2 3 4 | map '/posts' do run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['first_post', 'second_post', 'third_post']] } end |
use
This indicates that Rack uses certain middleware.
So what else do you need to know? Let’s take a closer look at environment hash function and response array.
Environment Hash
Rack server objects receive in a hash environment. What’s in that hash? Here are some more interesting parts:
REQUEST_METHOD
: The HTTP verb of the request. This is required.PATH_INFO
: The request URL path, relative to the root of the application.QUERY_STRING
: Anything next? in the request URL string.SERVER_NAME
andSERVER_PORT
: Server address and port.rack.version
: The version of the rack in use.rack.url_scheme
: Is it http or https?rack.input
: An IO-like object that holds raw HTTP POST data.rack.errors
: A feedback object for placing, writing, and discharging.rack.session
: The key value store to store the requested session data.rack.logger
: An object can write interfaces. It must implement informational, debugging, warning, error and critical methods.
A lot of frameworks built on Rack wrap env hash in a Rack::Request
object. This object provides a lot of convenience methods. For example, request_method, query_string, session, and logger return the values from the keys described above. It also allows you to test things like parameters, HTTP schema or if you are using ssl.
Response
When your Rack object returns a response, it must contain three parts:
- Status
- Header
- Body
Like the request, there is a Rack :: Response object that gives you convenient methods like write, set_cookie, and finish. Alternatively, you can return an array containing three elements.
Status
It’s an HTTP Status like 200 or 400
Header
Something responds to each and generates key-value pairs. The keys should be string and conform to the RFC7230 token specification. This is where you can set Content-Type
and Content-Length
if it fits your answer.
Body
The body is the data that the server sends back to the requestor. It must satisfy each and every string value brought.
What is Middleware?
One of the things that makes Rack so great is how easy it is to add chain middleware components between the web server and the app to customize how your request / response works.
But what is the middleware component?
An middleware component resides between the client and the server, handling both inbound and outbound responses. Why do you want to do that? There are plenty of middleware components available to Rack to eliminate guesswork from problems like caching activation, authentication, and spam traps.
Use Middleware in the Rack application
To add middleware to the Rack application, all you have to do is ask Rack to use it. You can use many middleware components and they will change request or response before passing it on to the next component. This component chain is called the middleware stack
.
Warden
We’ll look at how you add the Warden to a project. The Warden has to come after some sort of session middleware in the stack, so we will also use Rack::Session::Cookie
.
First, add it to your Gemfile project with the “warden” gem and install it with the package installed.
Now add it to your config.ru file:
1 2 3 4 5 6 7 8 9 10 11 12 13 | require "warden" use Rack::Session::Cookie, secret: "MY_SECRET" failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] } use Warden::Manager do |manager| manager.default_strategies :password, :basic manager.failure_app = failure_app end run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack'd']] } |
Finally, run the app with the rackup. It will find config.ru and boot on port 9292.
Note that there are many settings regarding having the Warden actually authenticate with your application. This is just one example of how to load it into the middleware stack. For a more robust example of Warden integration, check out this gist.
There is another way to define middleware stack. Instead of calling use directly in config.ru , you can use Rack::Builder
to wrap some middleware and application (s) in a large application.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] } app = Rack::Builder.new do use Rack::Session::Cookie, secret: "MY_SECRET" use Warden::Manager do |manager| manager.default_strategies :password, :basic manager.failure_app = failure_app end end run app |
Rack Basic Auth
One useful middleware is Rack::Auth::Basic
, which can be used to secure any Rack application with HTTP basic authentication. It is really lightweight and useful for protecting small bits of the application. For example, Ryan Bates uses it to protect the Resque server in a Rails application in this Railscasts article.
Here’s how to install it:
1 2 3 4 | use Rack::Auth::Basic, "Restricted Area" do |username, password| [username, password] == ['admin', 'abc123'] end |
Use Middleware in Rails
So what? Rack is pretty cool. And we know that Rails is built on top of it. But just because we understand what it is, it’s not really that useful when working with a production application.
How Rails uses Rack
Have you ever noticed that there is a config.ru
file in the root directory of every Rails project generated? Have you ever looked inside? Here’s what it contains:
1 2 3 4 5 | # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails.application |
This is quite simple. It finds the config/environment
file, then starts Rails.application
.
Wait, what is that? Looking at config / environment, we can see that it’s defined in config / application.rb. The config / environment is just the initial call! up there.
So what’s in config / application.rb? If we pay attention, we will see that it loads in the associated gems from config/boot.rb
, requests rails/all
, uploads the environment (development, test, production, etc.) and specifies the namespace version. of our application.
It looks like this:
1 2 3 4 5 6 | module MyApplication class Application < Rails::Application ... end end |
This means that Rails::Application
is a Rack application. Certainly, if we check the source code, it will respond to a call
But which middleware is it using? If we look closely, we see it’s automatically loading rails / application / default_middleware_stack – and checked, it looks like it was defined in ActionDispatch
.
Where does ActionDispatch come from? ActionPack
.
Action Dispatch
ActionPack is the framework of Rails for handling web requests and responses. ActionPack is home to pretty much all the good features you find in Rails, such as routing, the abstract controls you inherit from, and view rendering.
The most relevant part of ActionPack for our discussion here is Action Dispatch. It offers several middleware components handling ssl, cookies, debugging, static files and more.
If you look at each component of the ActionDispatch middleware, you’ll notice they all conform to the Rack specification: They all respond to the call, use the application and return the status, title and content. Many of them also use Rack :: Request and Rack :: Response objects.
Reading through the code in these components has helped you uncover many of the mysteries of what’s going on behind the scenes when making a request for a Rails application. When I realized that it was just a bunch of Ruby objects conforming to the Rack specification – passing requests and responses to each other – that made this whole part of Rails a lot less mysterious.
Now that we understand a bit of what’s going on, let’s take a look at how to actually bring some custom middleware into the Rails application.
Manually add Middleware
Imagine you are hosting an application on Engine Yard. You have a Rails API running on one server and a client-side JavaScript application running on another server. The API has a url of https://api.example.com and the client-side application is available at https://app.example.com .
You will quickly run into a problem: You cannot access the resource at api.example.com from your JavaScript application due to the same origin policy. As you probably know, the solution to this problem is to enable cross-origin resource sharing (CORS). There are many ways to enable CORS on your server – but one of the easiest is to use the middleware Rack :: Cors gem.
Start by asking for it in Gemfile:
1 2 | gem "rack-cors", require: "rack/cors" |
Like many other things, Rails provides a very easy way to download middleware. While we can certainly add it to the Rack :: Builder block in config.ru – as we did above – the convention of Rails is to put it in config / application.rb, using the following syntax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | module MyApp class Application < Rails::Application config.middleware.insert_before 0, "Rack::Cors" do allow do origins '*' resource '*', :headers => :any, :expose => ['X-User-Authentication-Token', 'X-User-Id'], :methods => [:get, :post, :options, :patch, :delete] end end end end |
Note that we are using insert_before here to make sure that Rack :: Cors comes before the rest of the middleware brought into the stack by ActionPack (and any other middleware you may be doing. use).
If you restart the server, you should get started! Your client-side application can access api.example.com without encountering a native policy JavaScript error.
Conclude
In this post, we will take a deep look at how Rack works and request / response loops for several Ruby web frameworks, including Ruby on Rails.
I hope that better understanding the process of a request to access your server and your application returning the response will help make things clear. And when we run into magic problems, we can easily judge it through how Rack works.