Rails from Request to Response: Part 2 - Routing
29 Dec 2013
This is part 2 of a series taking an in-depth look at how the internals of Rails handle requests and produce responses - you can read part 1 here.
Last time we took a brief look at how Unicorn workers accept clients from a shared socket and call our Rails application, and how requests bubble down through the middleware stack before arriving at Blog::Application.routes
. We’re now at the routing stage, where we need to match the request URL to a controller action and invoke it to get a response.
As mentioned, the #call
method will be invoked on whatever the Blog::Application.routes
method returns (as it happens, most of this post is actually just tracing the #call
method to the point where we get a response).
The definition of Blog::Application.routes
is located in the same engine.rb file:
# rails/railties/lib/rails/engine.rb
def routes
@routes ||= ActionDispatch::Routing::RouteSet.new
@routes.append(&Proc.new) if block_given?
@routes
end
So Blog::Application.routes
returns an instance of ActionDispatch::Routing::RouteSet
. This is the first reference we’ll see to ActionDispatch, which is a module that’s part of ActionPack. ActionDispatch handles tasks such as routing, parameter parsing, cookies, and sessions. ActionPack is one of the top-level gems in the Rails source code, and encompasses the ‘VC’ in Rails MVC - it handles routing as mentioned previously, as well as controller definitions (ActionController) and view rendering (ActionView). The RouteSet
class appears to be the pairing of a table of routes with a router to interpret and dispatch requests to controller actions - we’ll see more on these parts later.
Back to our engine - we need to examine the #call
method for the RouteSet
. The code lives in actionpack/lib/action_dispatch/routing/route_set.rb
. Here’s the relevant code:
# rails/actionpack/lib/action_dispatch/routing/route_set.rb
module ActionDispatch
module Routing
class RouteSet
def call(env)
@router.call(env)
end
...
end
end
end
So the RouteSet hands things off to whatever’s in @router
. If we look at the RouteSet constructor, we can see that @router
is an instance of Journey::Router
.
Journey
Journey is the core routing module in ActionDispatch. Its codebase is fascinating to browse as it actually uses a generalized transition graph (GTG) and non-deterministic finite automata (NFA) to match URLs and routes. Pull out those theoretical CS textbooks! Journey even includes a full-blown yacc grammar file for parsing routes! I’ll be writing more about Journey in future posts.
The #call
method in Journey::Router
is a bit lengthy, but here are the relevant bits:
# rails/actionpack/lib/action_dispatch/journey/router.rb
module ActionDispatch
module Journey
class Router
def call(env)
find_routes(env).each do |match, parameters, route|
env[@params_key] = (set_params || {}).merge parameters
status, headers, body = route.app.call(env)
return [status, headers, body]
end
end
...
end
end
end
This is the same Rack interface we saw in the Unicorn #process_client
code in part 1 - this is the heart of Rails, and all other Rack-compatible frameworks.
#find_routes
runs the URL through the GTG simulator using the routing table - it’ll take a separate post to explain the mechanism, but as its name suggests it returns a list of routes that match the requested URL. The routing table contains all of the routes of the system (instances of Journey::Route
), and is itself an instance of Journey::Routes
, which is constructed by the code you define in config/routes.rb
. The construction of the routing table and the ActionDispatch::Routing::Mapper
class could fill a post alone, and I highly recommend RailsCasts #231 and #232 for anyone looking for a detailed look at route construction.
Once we’ve found a matching route, we call #call
on the app associated with the route. Now, whenever we see a reference to app
in the Rails source, it’s a safe bet to assume that it refers to a Rack app. As it turns out, controller actions act like Rack apps! We’ll see more on this later on.
Tracking down where the app associated with a route gets initialized is a little tricky. The short of it is that when constructing the routing table, the ActionDispatch::Routing::Mapper
class calls add_route
, which will construct a Journey::Route
instance associated with a controller endpoint (the Rack app) and add it to the routing table. However, the class definition in mapper.rb
is almost is almost 2000 lines! Here’s the shortened definition of Mapper#add_route
:
# rails/actionpack/lib/action_dispatch/routing/mapper.rb
module ActionDispatch
module Routing
class Mapper
def add_route(action, options)
path = path_for_action(action, options.delete(:path))
...
mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
...
end
end
end
Here, @set
is the RouteSet
we were dealing with previously, and a little digging reveals that the app
referenced here is an instance of ActionDispatch::Routing::Dispatcher
. The Dispatcher
class is defined back in route_set.rb
- here’s its (simplified) definition of #call
. To make things more clear, imagine that we’re browsing to the /posts
URL of our blog app, which routes to the index action on the Posts controller.
# rails/actionpack/lib/action_dispatch/routing/route_set.rb
module ActionDispatch
module Routing
class Dispatcher
def call(env)
params = env[PARAMETERS_KEY]
prepare_params!(params)
# params = {:action=>"index", :controller=>"posts"}
controller = controller(params, @defaults.key?(:controller))
# controller = PostsController
dispatch(controller, params[:action], env)
end
...
end
end
end
We’re getting to the good stuff! At this point the controller
method has taken the interpreted controller parameter (such as "posts"
) and given us a reference to the class (PostsController
), and we have a basic params
hash. We’re ready to call the action on the controller and get our full-blown response - here’s the definition of the dispatch
method:
# rails/actionpack/lib/action_dispatch/routing/route_set.rb
def dispatch(controller, action, env)
controller.action(action).call(env)
end
At this point we’ve successfully matched the route to a controller/action, and the request is sent off into the ActionController stack, which will be the subject of the next post. Take a deep breath - routing is a particularly complex topic within Rails and the method chain can get pretty huge. Of course, as mentioned it’s not necessary to fully understand every detail of Rails routing. Part of the magic of Rails is that it hides all of these messy details from you! However, I find it fascinating to trace through and see the full flow of execution.
Some key takeaways from this post are the role of ActionPack in handling web requests from start to finish, and how controller actions behave like simple Rack endpoints. You can test this one for yourself by opening up rails console
and running something like:
2.0.0-p247 :001> PostsController.action("index")
=> #<Proc:0x007fa9fd356d40@rails/actionpack/lib/action_controller/metal.rb:269>
If you were to #call
that with the proper env
hash, you’d get the usual array of [status,headers,body]
!
In the next post, we’ll dive into ActionController, metal and all. Until next time!
Tweet