Examining The Internals Of The Rails Request/Response Cycle
Come with me on a journey through the internals of Rails, as we trace a request from the web server to the controller action method, and follow the response back again. This will give you a glimpse of how Rails works under the hood, hopefully exposing some of the “magic” of the framework. We will also get to see some of the design/architectural decisions at the core of Rails.
This analysis was conducted against a newly generated Rails 5.0.0.1 app.
Prerequisite: Rack
If you already understand Rack, you can skip this section.
Because Rails is built on top of Rack, this article isn’t going to make much sense unless you know Rack. I highly suggest reading the official specification (it’s fairly short) but I will give you a quick crash course here.
Rack apps are objects that handle web requests, and return responses. This is a simple Rack app class:
class ComplimentApp
def call(env)
[200, {'Content-Type' => 'text/plain'}, ["I like your shoes"]]
end
end
There is only one method that a rack app must implement: the #call
method.
This method receives the web request as an argument and returns an array as the response.
The request env should not be confused with the ENV
constant.
The request env represents a HTTP request.
The ENV
constant is a global built in to Ruby, and contains all the environment variables for the current process.
The web request is represented as a hash. This hash is referred to as the “request environment,” usually shortened to just “env.” It contains all the information about the request, like the HTTP method, the server’s hostname, the URL path, and so on.
The response is represented as a three-element array.
The first element is the HTTP status code.
The second element is a hash of HTTP headers.
The third element is the body of the response, which can be any object that responds to #each
, yielding only strings.
In the example above, the response body is an array with a string in it, which is valid.
This is how Rails, and most other Ruby web frameworks, interact with web servers. When the web server receives a HTTP request, it converts that request into an env hash, and calls your rack app. The rack app returns a response, which the web server sends back to the browser.
1. The Rack Entry Point
Rack apps define their entry point in the config.ru
file, and Rails is no different.
Looking inside the default config.ru
file we find this:
require_relative 'config/environment'
run Rails.application
The Rails.application
call returns an instance of your application class, defined in config/application.rb
.
I named my app “Endoscopy”, so my object is an instance of Endoscopy::Application
.
As with all rack apps, this application object must respond to the #call
method.
This is implemented in the Rails::Engine
class, which your application class inherits from, and it looks like this:
def call(env)
req = build_request env
app.call req.env
end
The build_request
call merges a bunch of values into the env.
These values are accessible to all the middleware and controllers.
Here is a list of keys that are added:
action_dispatch.parameter_filter
action_dispatch.redirect_filter
action_dispatch.secret_token
action_dispatch.secret_key_base
action_dispatch.show_exceptions
action_dispatch.show_detailed_exceptions
action_dispatch.logger
action_dispatch.backtrace_cleaner
action_dispatch.key_generator
action_dispatch.http_auth_salt
action_dispatch.signed_cookie_salt
action_dispatch.encrypted_cookie_salt
action_dispatch.encrypted_signed_cookie_salt
action_dispatch.cookies_serializer
action_dispatch.cookies_digest
action_dispatch.routes
ROUTES_70198900278360_SCRIPT_NAME
ORIGINAL_FULLPATH
ORIGINAL_SCRIPT_NAME
The application object is a kind of rack middleware itself.
It adds things to the env hash, and then passes it along to the next rack app.
In this case, the next app is an instance of Rack::Sendfile
, which indicates that the request is entering the middleware stack.
2. Middleware
Rack middleware are app objects that call other app objects. This allows middleware to do two things:
- Modify the env before it is passed to the next app
- Modify the response returned from the next app
Rails provides a bunch of middleware that are enabled by default. Here is an ordered list of all the middleware in the development environment:
- Rack::Sendfile – Makes responses from files on disk
- ActionDispatch::Static – Responds to requests for static files
- ActionDispatch::Executor – Undocumented ¯\_(ツ)_/¯
- ActiveSupport::Cache::Strategy::LocalCache::Middleware – Response caching
- Rack::Runtime – Response time measurement
- Rack::MethodOverride – Overrides the HTTP request method based on the
_method
param - ActionDispatch::RequestId – Gives each request a unique id
- Sprockets::Rails::QuietAssets – Silences logging on requests for sprockets assets
- Rails::Rack::Logger – Request logging
- ActionDispatch::ShowExceptions – Makes responses for unhandled exceptions
- WebConsole::Middleware – Interactive console for running code on the server
- ActionDispatch::DebugExceptions – Logging and debug info pages for unhandled exceptions
- ActionDispatch::RemoteIp – Determines the IP address of the client
- ActionDispatch::Callbacks – Runs callbacks before/after/around each request
- ActiveRecord::Migration::CheckPending – Raises an exception if there are pending migrations
- ActionDispatch::Cookies – Cookie serialization and encryption
- Rack::Session::Abstract::Persisted – Session management
- Rack::Head – Removes response body from HEAD requests
- Rack::ConditionalGet – HTTP caching
- Rack::ETag – HTTP caching
Once the request has passed through all 20 middleware objects, it then enters the router.
3. Routing
All of your app’s routes are stored inside an instance of ActionDispatch::Routing::RouteSet
at runtime.
This object also doubles as a rack app that dispatches the requests to the correct controller and action method.
This is the app that gets called after all the middleware.
The first thing that happens is that the request env gets converted into an ActionDispatch::Request
object.
Whereas env hashes are generic representations of a web request, these request objects contain functionality that is specific to Rails.
The request object is then used to lookup the correct route to dispatch to, which includes the corresponding controller class and action method.
Next, an empty response object is created, which is an instance of ActionDispatch::Response
.
Lastly, the controller is invoked via the #dispatch
class method, like so:
controller_class.dispatch(action, request, response)
Routing is implemented in a way that is similar to Rack middleware, but not quite the same. The router does not dispatch to other rack apps, it dispatches to controller classes. Controller classes are not rack apps – they return Rack-compatible responses, but are called via a different API.
4. The Controller
The entry point for the controller is the #dispatch
class method, which is implemented in ActionController::Metal
.
This is the implementation:
def self.dispatch(name, req, res)
if middleware_stack.any?
middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
else
new.dispatch(name, req, res)
end
end
In addition to the middleware stack that we saw earlier, each controller class can have its own stack of middleware. In the new Rails 5 app that I’m examining, however, controller classes have no middleware by default.
Middleware or not, the controller class creates a new instance of itself, and forwards the arguments to the #dispatch
instance method.
This means each request is handled by a new, clean controller object.
Here is the implementation of the #dispatch
instance method, from ActionController::Metal
:
def dispatch(name, request, response)
set_request!(request)
set_response!(response)
process(name)
request.commit_flash
to_a
end
The first thing the controller object does is store the request and response objects. This makes sense, as these two can be accessed from any method within the controller.
Then comes the process(name)
call, which eventually calls the correct action method – code that you have written yourself.
Before that, however, the process
call makes its way through several places in the controller’s very deep inheritance hierarchy.
How deep is “very deep”?
Have a look for yourself:
ExampleController.ancestors.size #=> 68
ExampleController.ancestors #=>
# [ExampleController, #<Module:0x007fe42c6632b8>, ApplicationController,
# #<Module:0x007fe42b6acd10>, #<Module:0x007fe42b855018>,
# #<Module:0x007fe42b855040>, ActionController::Base, Turbolinks::Redirection,
# Turbolinks::Controller, ActiveRecord::Railties::ControllerRuntime,
# ActionDispatch::Routing::RouteSet::MountedHelpers,
# ActionController::ParamsWrapper, ActionController::Instrumentation,
# ActionController::Rescue,
# ActionController::HttpAuthentication::Token::ControllerMethods,
# ActionController::HttpAuthentication::Digest::ControllerMethods,
# ActionController::HttpAuthentication::Basic::ControllerMethods,
# ActionController::DataStreaming, ActionController::Streaming,
# ActionController::ForceSSL, ActionController::RequestForgeryProtection,
# AbstractController::Callbacks, ActiveSupport::Callbacks,
# ActionController::FormBuilder, ActionController::Flash,
# ActionController::Cookies, ActionController::StrongParameters,
# ActiveSupport::Rescuable, ActionController::ImplicitRender,
# ActionController::BasicImplicitRender, ActionController::MimeResponds,
# AbstractController::Caching, AbstractController::Caching::ConfigMethods,
# AbstractController::Caching::Fragments, ActionController::Caching,
# ActionController::EtagWithTemplateDigest, ActionController::ConditionalGet,
# ActionController::Head, ActionController::Renderers::All,
# ActionController::Renderers, ActionController::Rendering, ActionView::Layouts,
# ActionView::Rendering, ActionController::Redirecting,
# ActiveSupport::Benchmarkable, AbstractController::Logger,
# ActionController::UrlFor, AbstractController::UrlFor,
# ActionDispatch::Routing::UrlFor, ActionDispatch::Routing::PolymorphicRoutes,
# ActionController::Helpers, AbstractController::Helpers,
# AbstractController::AssetPaths, AbstractController::Translation,
# AbstractController::Rendering, ActionView::ViewPaths,
# #<Module:0x007fe42b67f478>, ActionController::Metal, AbstractController::Base,
# ActiveSupport::Configurable, ActiveSupport::ToJsonWithActiveSupportEncoder,
# Object, ActiveSupport::Dependencies::Loadable, PP::ObjectMixin,
# JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, Kernel,
# BasicObject]
Below is the call stack between the #dispatch
instance method and the action method.
Keep in mind that these are all methods called on a single controller object.
ActionView::Rendering#process
– Sets/restores global I18n configAbstractController::Base#process
– Resets@_action_name
and@_response_body
, and raises an exception if the action is not found.ActiveRecord::Railties::ControllerRuntime#process_action
– Resets runtime measurements for ActiveRecord query loggingActionController::ParamsWrapper#process_action
– Wraps params inside a hashActionController::Instrumentation#process_action
– Per-request instrumentationActionController::Rescue#process_action
– Callsrescue_from
blocks when exceptions are raised.AbstractController::Callbacks#process_action
– Runs callbacks before/after/around the action methodActionController::Rendering#process_action
– Setsself.formats
AbstractController::Base#process_action
– Does nothing except call#send_action
ActionController::BasicImplicitRender#send_action
– Renders the implicit view, if nothing was explicitly rendered in the action method
After all of that, the action method is finally called. On to rendering!
5. Rendering
This is the controller implementation:
class ExampleController < ApplicationController
def index
render plain: 'Hello, world!'
end
end
I’m going to skip over the details of view rendering – that could be an article in itself.
No matter what you render, the end result is stored in response.body
.
It also sets the Content-Type
HTTP header on the response object, to text/plain
in this case.
That’s it for the controller. Now we unwind the stack, and see what happens on the way out.
6. Leaving The Controller
After control has left the action method, the call stack unwinds back through the controller class heirarchy. In order:
after_action
callbacks are run.- Instrumentation finishes.
- Global I18n config is restored to its original value.
- Flash messages are stored within the session.
Lastly, the controller provides a return value by calling response.to_a
.
This converts the response object into a Rack-compatible response array, which then flows back through the router and all the middleware.
7. Leaving Routing
Routing simply passes the controller’s return value back to the middleware, unless the response headers include X-Cascade: pass
.
When a contoller returns this header, it is basically saying “nah, that request isn’t for me, so try find someone else to handle it.”
The router then attempts to find another matching route, and re-dispatch the request.
This seems to be a little-known, and rarely used feature.
8. Leaving Middleware
After leaving routing, the Rack response array is returned through all the middleware. During this phase, middleware have the opportunity to modify the status code, headers, and response body. All the middleware are explained earlier in this article, but the notable response modifications are:
- The
ETag
header is set, based on the response body - The response body may be removed entirely, based on HTTP caching headers
- The session is “committed,” meaning that is serialized and stored in a cookie
- Cookies are serialised, and added to the response headers
- Information about the request is logged out
After all the middleware, the Rack-compatible response is returned from your app to the web server. There, it is serialized into a HTTP response string, and sent back to the client.
Conclusion
The Rails request/response cycle is heavily based upon Rack. The root application object is a rack app, and a bunch of Rails functionality is implemented as Rack middleware.
Routing dispatches requests to a class method on the controller. The controller class then instantiates itself, and dispatches the request to the newly created object.
While controllers are not Rack apps, they are quite similar. Action methods do not return a response directly – they mutate a response object, which is later converted into a Rack-compatible response array, and then returned. There is potentially a Rack middleware stack specific to each controller class.
Controllers inherit from 67 ancestor classes/mixins. This probably contributes to the impression that Rails is full of “magic.”
Got questions? Comments? Milk?
Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).