What Rack is
A relay race has several runners, not one. Each runs a leg and hands the baton to the next. From the stands it looks like one continuous run. Up close it's a chain of handoffs, and any runner can slow the baton down, speed it up or refuse to pass it on. A Rails request works the same way. "Rails" isn't one thing processing your request. It's a chain of small objects passing the request along, and the last one in the chain calls your routes.
The contract: one method, three things back
Rack is the library underneath every Rails app, and its contract is small on purpose. Any object that responds to call(env) and returns [status, headers, body] is a valid Rack app:
envis a Hash carrying the whole request:env["REQUEST_METHOD"]("GET","POST"...),env["PATH_INFO"]("/catalog/roadmap"),env["rack.input"](the request body, as an IO-like object), plus dozens more keys added by the middleware that ran before you.statusis an Integer (200,404,302).headersis a Hash from header name to value. This app runs Rack 3 (Gemfile.lockpinsrack (3.2.5)). In Rack 3, header names must be lowercase, and a value is either a String or, for a header with several values, an Array of Strings. A value containing a newline is invalid.bodyresponds toeachand yields String chunks. Rack 3 also allows a body that responds tocall(stream)for streaming, buteachis the form you'll write by hand.
That's the whole interface. There's no required superclass; respond_to?(:call) plus the right return shape is enough. Rails.application is itself an object that meets this contract, through Rails::Engine#call. That it routes to your controllers is a detail of how it answers call(env), not a different interface.
Rack::Lint, which checks an app against the spec, shows the header rule directly:
{ "x-a" => "1\n2" } -> Rack::Lint::LintError: invalid header value x-a: "1\n2"
{ "x-a" => ["1", "2"] } -> passes
Your app is a stack
Run bin/rails middleware in this app, in development, and you get the chain it builds on every boot:
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use Propshaft::Server
use ActionDispatch::Executor
use ActionDispatch::ServerTiming
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Rack::Attack
use OmniAuth::Builder
run NewbieOnRails::Application.routes
That's 29 use lines before the single run line that dispatches to a controller. Each one is a small Rack app wrapping the next. HostAuthorization calls Sendfile, which calls Static, which calls Propshaft::Server, and so on down to run, the only line in the chain that doesn't wrap anything. ActionDispatch::Session::CookieStore reads and writes the cookie your app calls a session. ActionDispatch::Flash is what lets flash[:notice] survive a redirect. Neither is built into Rails's core; both are middleware, in this list and in this order, each doing one job and handing off.
The production stack is shorter. It drops the development helpers, such as WebConsole::Middleware, ActionDispatch::Reloader and ActiveRecord::Migration::CheckPending, and adds ActionDispatch::AssumeSSL and ActionDispatch::SSL near the top.
Why the order in that list matters
Each middleware only sees what the ones before it passed along, and it only acts before the ones after it run. ActionDispatch::Flash keeps its messages in the session, so it has to sit below ActionDispatch::Session::CookieStore; above it, there would be no session to read. ActionDispatch::RequestId runs before Rails::Rack::Logger, which is why the production log can tag each line with the request's id. Position decides whether a middleware works or never sees the data it needs. The rest of this course is about writing your own link in this chain: where it goes, what it may do to a request on the way in and a response on the way out, and how to test it without starting a server.
Key takeaways
- Rack's whole contract is one method:
call(env)in,[status, headers, body]out. No base class is required, only the right shape. envis a Hash carrying the request;statusis an Integer;headershas lowercase names and String values, or an Array of Strings for several values;bodyresponds toeach.- In development, this app's
bin/rails middlewareoutput has 29uselines before the singlerunline that dispatches to your routes. Rails is a stack, not a monolith. - Order in that stack is functional: a middleware can only use what the ones above it already added to
env, and can only act before the ones below it run.
Keep going with lesson 2
Writing a middleware: initialize and call
Builds a timing middleware with initialize and call, and shows what breaks when @app.call(env) is forgotten.
A free account also gives you the quizzes, your progress and notes, and the discussion.