Ruby on Rails' inside: ActionDispatch and Rack

Rails is quite a complex beast and often enough, we think of it as a MVC framework and nothing more. However, the MVC pattern is only the tip of the iceberg, it has a lot more going on before entering your controllers.

When you start using ruby on rails, its ease of use makes it great to get you started. Everything works out of the box. You may be using Devise for authentication, Cancan for authorization and every time you need something that you believe Rails do not provide, you search google for a gem doing what you seek.

And joy is everywhere, your app works fine, you didn't even need to write that much code to get where you are!

It's awesome.

Until ...

you need to change little things that are handled by the myriad of gems you use and you just don't know where to start looking. Should you fork the gem? Then I'll have to merge upstream changes. Maybe monkey patching? Updating the gems can break my monkey patch without notice...

It seems you are walking on eggs. You're not sure what does what, and monkey patching is not simple anymore as you don't know what implications a tiny change can have on your system. You feel trapped by all your previous choices.

You may be installing some gems for a reason: You just don't know enough how rails work so many small features seems hard to implement when in reality, it isn't. And it's normal, it's part of learning a new framework. The same would apply to any framework in any language. You have to try stuff before understanding them. And this post today is about understanding them.

If I say I'd need you to change a view, you probably would start looking at the controller, the views or the helpers and start making changes right away. However, if I'd ask you to implement or change something that has to do with:

You'd probably have to stop for a few seconds and scratch your head wondering where to start.

And this is what this post is all about. Giving you a better understanding of how Rails works so you can do the customization you need without breaking a sweat. Because there's a lot of stuff to cover, I won't cover it all on this post. The next few posts will cover different part of rails that handles the request up to the controller. For each part, I'll associate it a real use case so you can understand, and be able to use that knowledge in your application.

To kick this serie off, I'll start with authentication.

ActionView, ActionController, ActiveRecord and ?

I'm sure you know the three components I listed above. Did you know about ActionDispatch? Or Rack? These two bad boys are responsible for delivering a request to the proper controller.

Rack overview

Rack is webserver interface. Quite dry. In laymen's term, it means Rack is an editable list of component that every request goes through in order to build the response (HTML page).

When you start rails s, a web server (thin, unicorn, WebRick, Mongrel, etc.) is launched and receives request from browser. The web server then submit that requests to Rack which will process and and builds the response that the web server will send back to the client (i.e browser). If you were one of the lucky to attend Marc-André Cournoyer's free webinar on how to create a webserver, you know how this works. If not, don't worry: the server he built with us was released on github for your viewing pleasure.

When you create a new Rails project, it comes with 23 rack middlewares. Type rake middleware at the root of your rails project. Here's an example:

use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd148f9468>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run RackTest::Application.routes

Authentication is something I've talked before. When I first talked about it, I didn't cover rack's implication except for Warden's inclusion as a middleware. This time, I will explore the rack side of the authentication using Warden so you can browse between here and my other post if you need precision.

How to use Rack

To create a middleware, you need an object that responds to call(env) function and returns an array that will then be send to the browser. The array consists of exactly three things

  1. Status code (200, 302, 404, ...)
  2. A hash containing the the header
  3. The body (Needs to be enumerable via #each)

Here's an example of what a rack middleware can look like in its most basic form:

class MyMiddleware
  def call(env)
    [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]
  end
end

Notice the array that wraps the body "Hello Rack!". This is mandatory since ruby 1.9.x because String does not implement String#each. By wrapping the string in an array, the body can be processed by rack.

Let the hacking begin

Create a new rails application by typing rails new racktest.

Now create a folder in app/ named middleware. Then create a file called my_middleware.rb and fill it up:

# app/middlewares/my_middleware.rb
class MyMiddleware
  def initialize(app)
  end

  def call(env)
    [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]
  end
end

For rails to load the middleware, I have to configure rails to include our middleware in the stack. The way it works is that you have to tell Rails when to insert the middleware. Look at the list of middlewares to know before/after which existing middleware you want your own middleware to be. For this example, I want it to be the very first in the stack so I'll be adding it before ActionDispatch::Static. Here's how it looks like in config/environments/development.rb

# config/environments/development.rb
Racktest::Application.configure do
  # ... stuff
  config.middleware.insert_before ActionDispatch::Static, MyMiddleware
end

Now run rails s access your server from your browser. You'll have the welcome message from Rack. Whatever path you put after localhost:3000 won't change anything anymore... You broke rails. Why?

initialize(app)

Rack doesn't mention anything about this. The reason it's there is that Rails builds the middleware list through ActionDispatch::MiddlewareStack. By doing so, it adds another rule to the middleware:

MiddlewareStack initialize every middleware by passing an argument app to the object.

call(env)

The env variable is the current environment build for that request. It's the glue between middlewares, the only variable that will be persisted. If you set something in there, other middlewares will be able to access it, even your controller.

For example, Warden stores its manager into env['warden']. This is what you use when you call current_user from Warden or Devise.

Why does it break rails?

The reason why routes aren't working anymore is because the rack list is broken by our middleware. When I printed the middleware list from rack, it returned something that could be interpreted as an array. Therefore, you may think that middlewares are handled in a sequential manner:

17/rack_stack_1.jpg

In reality, rack middleware are implemented in a russian dolls structure:

17/rack_stack_2.jpg

That app parameter during the initialization is actually the next middleware in the chain. By not calling @app.call(env), rack was stop at the very first middleware (our own) and returned the value we set at line #7.

Here's what you want to do to continue down the stack:

# app/middlewares/my_middleware.rb
class MyMiddleware
  def initialize(app)
    @app = app #Store the app to call it down the stack
  end

  def call(env)
    # Initialize stuff before entering 'rails'
    # Retrieve a connection form a pool (Redis, Memcache, etc.)
    # Authentication/authorization/tenancy setup needs to be done *before*
    # Remember, you can set stuff in env and then access it in your controller.

    # The response has the same structure as before:
    # [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]
    # The header is now fully populated and instead of the "Hello Rack!",
    # the body is a full HTML page.

    # @app.call will call ActionDispatch::Static which, in turn, will call ActiveSupport::CacheStrategy which will
    # call Rack::Runtime and so on up to your controller/view.
    response = @app.call(env)

    # Analytics could go here.
    # If you want to throttle connection, you could increment it here too
    response
  end
end

Restart your server and access it with your browser. Rails now works as expected. And this is why Rack is so powerful, it makes it easy to add functionality wherever you want on the stack without affecting the other moving parts.

Authentication & Authorization

Those two principles share a similar behavior, a request is made to access a page, if the user is authenticated and authorized, the web page is returned. If not, a fallback situation is raised. It can be a login page, a redirection with a flash message, anything.

With rack, it's quite simple to achieve that.

# app/middlewares/my_middleware.rb
class MyMiddleware
  def initialize(app)
    @app = app #the next middleware to be called
    @fallback = RestrictedController
  end

  def call(env)
    result = catch(:restricted) do
      @app.call(env)
    end

    if result.nil?
      @fallback.call(env)
    else
      result
    end
  end
end
# app/controllers/application_controller.rb
class ApplicationController > ActionController::Base
  def restrict!
    throw :restricted
  end
end

# app/controllers/restricted_controller.rb
class RestrictedController > ActionController::Base
  def self.call(env)
    action(:respond).call(env)
  end

  def respond
    flash.alert = "Couldn't access the resource"
    redirect_to root_url
  end
end

The middleware will call the class method RestrictedController.call. From there, the controller is instantiated RestrictedController#respond is the called through that class method. From within RestrictedController#respond, you have access to everything you would normally have within any other controller.

In any of your controller, if you call restrict!, it will throw an error that will be caught (:restricted) by your rack application. When the error is raised, the fallback application will be called.

Obviously, you have to use a symbol that is not caught by any other gem/library. If the symbol is caught somewhere else, the result will be undefined. Good news is that rails uses Exception class most of the time, so most of the symbols are available to you.

This is exactly how Warden works. When you make a request to the server, Warden checks if there's a user_id in the session, if there is, it sets current_user to the proper User. If not, it will use the strategies you set up to try to authenticate the user. If the authentication fails, Warden throw :warden which is caught in its middleware. It then call the rack application failure_app and finishes the request execution through that middleware.

If you want to know more about setting up your own authentication, you may want to read my previous post.