You don’t need to know this.
Really.
But if you’re like me, you find it hard to step off the cliff and walk across the open air—even if all your friends are doing it—without at least some understanding of the magic that makes it all work.
The big picture is this:
A request comes in to the webserver. In the normal course of things, it winds up resolving to something in the public directory of your application, which contains an .htaccess file that redirects it to the CGI dispatcher script public/dispatch.cgi (or public/dispatch.fcgi if you’re using fast CGI) unless the requested file already exists (e.g. index.html). The dispatcher invokes some libraries that use the contents of your config/routes.rb to decide what controller to use and what method of the controller to call.
If that satisfies you, you’re quite welcome, and we hope you enjoy the rest of your Rails experience.
For the rest of you, that just must know the details, I won’t bother reminding you that you probably don’t need to know them because it’s unlikely to work any better on you than it did on me. And who knows, maybe you do need to know it, in your pursuit of some grand leap of Rails wizardry that will leave us all slack-jawed in amazement? Who am I to stand in the way of that?
So here goes. The details are both nitty and gritty, but at least when we’re done you’ll know what’s happening.
Let’s say, for example, that it’s Apache (though it could just as well be WEBrick or Lighttpd—the ideas are the same even if the syntax is a little different).
The request contains a URL such as “/recipes/show/1” or “/rails/new/UnderstandingHowRequestsAreRouted.”
If you followed along in HowtoSetupApacheWithFastCGIAndRubyBindings or something similar1, your httpd.conf file will contain something like this (omitting the lines that don’t affect the routing):
<Directory /var/www/>
AllowOverride all
</Directory>
<VirtualHost *:80>
DocumentRoot /var/www/rails/testapp/public
</VirtualHost>
1 (Slightly more complex configurations, such as described in HowtoDeployMoreThanOneRailsAppOnOneMachine, are explained below, after we’ve got the basics down.)
This tells Apache to treat all URLs as if they were relative to the directory /var/www/rails/testapp/public (so our example URLs would be converted into “/var/www/rails/testapp/public/recipes/show/1” and “/var/www/rails/testapp/public/rails/new/UnderstandingHowRequestsAreRouted.”
respectively.
It also tells Apache that directories under /var/www/ (which these are) have their own special per-directory instruction that can override anything they want to. How do they do that?
It lives in the public directory of your Rails App, and it looks something like this (again, omitting lines that don’t concern us here):
# Redirect all requests not available on the filesystem to Rails
RewriteEngine On
RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
This is where we override any the base Apache configuration to set off the Rails magic. In order, the RewriteRules:
dispatch.cgiThere are a few things to note here:
<pre>
AddHandler fastcgi-script .fcgi
AddHandler cgi-script .cgi
Options +FollowSymLinks +ExecCGI
</pre>
These tell Apache that dispatch.cgi (or .fcgi) is a program, and that it should be run to handle the request.
In either case, it’s a very short program, and they are essentially the same, as far as routing is concerned:
#!/usr/local/bin/ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
Dispatcher.dispatch
Chunk by chunk:
config/environment.rb is required and should be treated as part of this filedispatcher.rb is required and should be treated as part of this file (and as we’ll see in a moment, that’s where the fun starts). Note that your copy may not be in the directory mentioned in the comment—if for no other reason than the path given is specific to Rails version 0.80 and you are almost certainly using something newer.This library is again pretty small (only a few dozen lines, including the copyright notice & OpenSource licence), but we only care about a small part of it (using ":" to indicate omited lines):
:
class Dispatcher
:
def dispatch(cgi = CGI.new, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS)
:
request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
prepare_application
: ActionController::Routing::Routes.recognize!(request).process(request, response).out
:
end
private
def prepare_application
ActionController::Routing::Routes.reload if Dependencies.load?
:
end
end
end
The omitted code is mostly error handling and logic to load/flush/reload base data when needed. Remember that your Rails App can accept changes “on the fly” without needing to restart or reconfigure anything? Some of that magic is handled here.
The only one we care about is the loading of routes (from your “config/routes.rb”). These are used by the ActionController when it’s asked to “recognize!” the request (which it constructs from the CGI request data that the script was passed by the web server—that’s where your (rewritten) path lives at this point in the process).
ActionController::Routing::Routes.recognize!At this point, the path of the request looks something like:
/recipes/show/1 (stored in request.request_uri) and the script name (which can be found in request.env["SCRIPT_NAME"] is something like /dispatch.cgi. Note that the script name is really the path to the script from the DocumentRoot…in other words, it starts with a “/”.
From these, the ActionController computes the path we care about by taking the request_uri and chopping off whatever is left of the DocumentRoot path—but since it doesn’t have access to Apache’s internal variables, it has to guess. So it assumes that, whatever the prefix that needs to be chopped off is, it will be the same for the URL and the script. So the code looks like this:
actionpack…/lib/action_controller/request.rb
# returns the interpreted path to requested resource after
# all the installation directory of this application was taken into account
def path
path = request_uri ? request_uri.split('?').first : ''
# cut off the part of the url which leads to the installation directory of this app
path[relative_url_root.length..-1]
end
# returns the path minus the web server relative
# installation directory
def relative_url_root
File.dirname(env“SCRIPT_NAME”.to_s).gsub /(\.$|\/$)/, ’’
end
And the result is a string such as “/recipes/show/1”, which winds up in being the request.path
The request path is then matched against the routes from your config/routes.rb until one of them matches. Note that this means the most general rules should come last, otherwise they will gobble up all the requests.
Using Introspection, the system can call…
If you’re running multiple Rails application on the same machine you’ve probably got a slightly more complicated situation…but only slightly. (see HowtoDeployMoreThanOneRailsAppOnOneMachine)
Typically, the http.conf configuration will not map the DocumentRoot to the public directory of your Rails application (for one thing, how would it know which one to use?). Instead, Rails requests will be rewriten individually by rules like the ones in the .htaccess file, with the end result that they end up mapping to the right public directory, just as if they’d come in the old fashioned way.
But dispatch.cgi was there all along, and since it didn’t go through the extra round of URL rewriting, it probably won’t wind up with the same directory prefix. Consequently when the ActionController trys to do it’s stuff, it will wind up chopping an arbitrary part of the URL off and…well, let’s just say it isn’t pretty.
Fortunately, Apache provides a directive (RewriteBase) to handle just this situation. It can be used in your .htaccess file to make the directory prefixes for the rewritten URLs and the dispatch.cgi match again, and from that point on everything works just as it did before.
category: Understanding