Understanding Rack explains cookies, sessions, CSRF and logging, and lets you add cross-cutting behaviour cleanly.
Rack is the interface between Ruby web servers and frameworks. A Rack app is any object with call(env) that returns [status, headers, body], where env is a hash describing the request. Rails itself is a Rack app, started from config.ru.
A request's journey:
- Puma accepts the connection and builds the
env. In production Rails 8 apps, Thruster sits in front of Puma, adding HTTP/2, compression, asset caching and X-Sendfile. - The middleware stack: each middleware wraps the next, can modify the
env, short-circuit with its own response, or modify the response on the way back.bin/rails middlewarelists them:ActionDispatch::HostAuthorization,Rack::Sendfile,ActionDispatch::Static,ActionDispatch::Executor,Rack::Runtime,ActionDispatch::RequestId,Rails::Rack::Logger,ActionDispatch::ShowExceptions,ActionDispatch::Cookies,ActionDispatch::Session::CookieStore,ActionDispatch::Flash,Rack::MethodOverride,Rack::ETagand more. - The router (
ActionDispatch::Routing::RouteSet, also a Rack app) matches the path and calls the controller action, itself a Rack endpoint.
Add your own middleware with config.middleware.use MyMiddleware, or position it with insert_before/insert_after, and remove one with config.middleware.delete. Middleware fits concerns that apply to every request regardless of controller: request timing, tenant detection from the host, blocking abusive IPs (often via rack-attack), or adding headers.
Routes can also mount any Rack app directly: mount Sidekiq::Web => "/sidekiq" or mount ->(env) { [200, {}, ["ok"]] } => "/ping".
Example
# app/middleware/request_timer.rb
class RequestTimer
def initialize(app) = @app = app
def call(env)
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, body = @app.call(env) # inner layers + Rails
ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round(1)
headers["server-timing"] = "app;dur=#{ms}"
[status, headers, body]
end
end
# config/application.rb
# config.middleware.insert_before Rack::Runtime, RequestTimer
# config/routes.rb: any Rack app can be mounted
# mount ->(env) { [200, { "content-type" => "text/plain" }, ["pong"]] }, at: "/ping"
# $ bin/rails middleware
# use ActionDispatch::HostAuthorization
# use Rack::Sendfile
# use ActionDispatch::Static
# use ActionDispatch::Executor
# ...
# use RequestTimer
# use Rack::Runtime
# ...
# run Blog::Application.routesRemember
- A Rack app is call(env) returning [status, headers, body]
- Rails is a middleware stack wrapped around the router; bin/rails middleware lists it
- Use middleware for cross-cutting, controller-independent behaviour
Watch out for
- Putting per-request logic that needs current_user or params into middleware; it runs before routing and controllers.