1. Introduction to Rack
Rack provides a modular interface for developing web applications in Ruby. By wrapping HTTP requests and responses using a conventional structure, it unifies the API for web servers, web frameworks, and software in between (known as middleware) into a single method call.
This allows Rack compliant web servers like Puma or Falcon to be interchangeably used with any Rack based web framework such as Rails.
Before diving into how Rails integrates with Rack, let's look at Rack itself.
1.1. A Basic Rack Application
A Rack app is an object which implements a call method. It is passed an env hash, known as the Rack environment.
Here's an example of a barebones Rack app:
class App
def call(env)
[200, { "content-type" => "text/plain" }, ["Hello World"]]
end
end
run App.new
When an HTTP request is made, the Rack-compliant web server parses it to create the env hash, and calls the application with env. The call method must return an array with exactly three elements, representing the HTTP response:
- The HTTP response code (
200in the above example). - A hash containing any HTTP response headers we wish to send.
- An enumerable object that yields strings, representing the response body.
Rack applications are generally run using the web server's command line program, with the entry point for the application being stored in a config.ru file:
$ cat > config.ru << APP
rack_app = lambda do |env|
[200, { "content-type" => "text/plain" }, ["Hello World"]]
end
run rack_app
APP
$ gem install puma
$ puma
Your app should be available at http://localhost:9292.
$ curl localhost:9292
Hello World
1.2. Rack Middleware
Rack applications can be wrapped using middleware which may operate upon a request before it reaches the main application, and again after the application has returned a response to the request. Middleware is usually used for tasks like logging, caching, authentication, and measuring performance.
A Rack middleware must have a new method that accepts the Rack app and any arguments used to configure the middleware. The new method must return a Rack application that responds to call. Typically, Rack middleware are classes, and each instance of the middleware wraps access to the related application:
class MyMiddleware
def initialize(app)
@app = app
end
def call(env)
# Operations before the request hits the main application
# -------------------------------------------------------
# Propgate the request down the middleware stack
status, headers, body = @app.call(env)
# ---------------------------------------
# Operations after the request comes back
# Propogate the response up the middleware stack
[status, headers, body]
end
end
Middleware can short-circuit the stack by skipping @app.call completely and returning a reponse by itself. This means the request never hits the main application or the remaining middleware in the stack. A middleware to authenticate a request might use this technique.
class AuthenticateRequest
def initialize(app)
@app = app
end
def call(env)
if authenticated?(env["HTTP_AUTHORIZATION"])
@app.call(env)
else
[401, { "content-type" => "text/plain" }, ["Authentication failed"]]
end
end
def authenticated?(token)
# ...
end
end
Middleware is added to a Rack app with use:
class AuthenticateRequest
# ...
end
class App
def call(env)
[200, { "content-type" => "text/plain" }, ["Hello World"]]
end
end
use AuthenticateRequest
run App.new
This DSL to construct Rack applications is provided by Rack::Builder. For further information about Rack, consult the Rack specification and Rack Website.
2. Rails on Rack
2.1. The Primary Rack Object
Rails.application is the primary Rack application object of a Rails
application. A Rack compliant web server should use the Rails.application object to serve a Rails application.
2.2. Starting the Rails Server
Rails subclasses Rackup::Server to create Rails::Server. bin/rails server instantiates a Rails::Server object and starts the web server.
Rails::Server.new.tap do |server|
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
end
See the initialization guide for further information on how the server starts up.
3. Action Dispatch Middleware Stack
ActionDispatch::MiddlewareStack is Rails' equivalent of Rack::Builder. It's built with more flexibility and features to meet Rails' requirements.
Rails::Application uses ActionDispatch::MiddlewareStack to combine internal and external middleware to build the stack which forms a complete Rack application using Rails.
3.1. Inspecting the Middleware Stack
View the middleware stack by running:
$ bin/rails middleware
Here's an example from a freshly generated Rails app:
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 Propshaft::QuietAssets
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
run MyApp::Application.routes
The Internal Middleware Stack section below summarizes the default middleware components depicted above.
3.2. Configuring the Middleware Stack
Rails provides a configuration interface config.middleware for adding, removing, and modifying the middleware stack via application.rb or the environment specific configuration file environments/<environment>.rb.
3.2.1. Adding Middleware
There are three methods to add new middleware to the stack.
config.middleware.use(new_middleware, args): Adds the new middleware at the bottom of the middleware stack.config.middleware.insert_before(existing_middleware, new_middleware, args): Adds the new middleware before the specified existing middleware in the middleware stack.config.middleware.insert_after(existing_middleware, new_middleware, args): Adds the new middleware after the specified existing middleware in the middleware stack.
Example usage:
# config/application.rb
# Push `Rack::BounceFavicon` at the bottom
config.middleware.use Rack::BounceFavicon
# Add `Lifo::Cache` after `ActionDispatch::Executor`.
# Pass { page_cache: false } argument to Lifo::Cache.
config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false
3.2.2. Swapping Middleware
Swap middleware using config.middleware.swap.
# config/application.rb
# Replace `ActionDispatch::ShowExceptions` with `Lifo::ShowExceptions`
config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
3.2.3. Moving Middleware
Move existing middleware components in the stack using config.middleware.move_before or config.middleware.move_after.
# config/application.rb
# Move ActionDispatch::ShowExceptions to before Lifo::ShowExceptions
config.middleware.move_before Lifo::ShowExceptions, ActionDispatch::ShowExceptions
# config/application.rb
# Move ActionDispatch::ShowExceptions to after Lifo::ShowExceptions
config.middleware.move_after Lifo::ShowExceptions, ActionDispatch::ShowExceptions
3.2.4. Deleting Middleware
Delete middleware using config.middleware.delete.
# config/application.rb
config.middleware.delete Rack::Runtime
Using delete! will raise an error if the middleware component doesn't exist.
# config/application.rb
config.middleware.delete! Some::NonExistentMiddleware
3.3. Reloading the Middleware Stack
The middleware stack is loaded once and isn't monitored for changes. Restart your server after making changes to your middleware stack.
3.4. Internal Middleware Stack
Much of Action Controller's functionality is implemented as middleware. The following list explains the purpose of each of them:
3.4.1. ActionDispatch::ActionableExceptions
ActionDispatch::ActionableExceptions provides a way to dispatch actions from Rails' error pages if the request is local.
3.4.2. ActionDispatch::Callbacks
ActionDispatch::Callbacks provides callbacks to be executed before and after dispatching the request.
3.4.3. ActionDispatch::ContentSecurityPolicy::Middleware
ActionDispatch::ContentSecurityPolicy::Middleware provides a DSL to configure a Content-Security-Policy header. See Securing Rails Applications for further information.
3.4.4. ActionDispatch::Cookies
ActionDispatch::Cookies reads cookie data from the request and writes cookie data on the response.
3.4.5. ActionDispatch::DebugExceptions
ActionDispatch::DebugExceptions is responsible for logging exceptions and showing a debugging page if the request is local.
3.4.6. ActionDispatch::Executor
ActionDispatch::Executor ensures thread safe code reloading during development.
3.4.7. ActionDispatch::Flash
ActionDispatch::Flash sets up the flash keys. Only available if config.session_store is set to a value.
3.4.8. ActionDispatch::HostAuthorization
ActionDispatch::HostAuthorization prevents DNS rebinding attacks by restricting the hosts to which a request can be sent. See the configuration guide for configuration instructions.
3.4.9. ActionDispatch::Reloader
ActionDispatch::Reloader provides prepare and cleanup callbacks, intended to assist with code reloading during development.
3.4.10. ActionDispatch::RemoteIp
ActionDispatch::RemoteIp checks for IP spoofing attacks.
3.4.11. ActionDispatch::RequestId
ActionDispatch::RequestId makes a unique X-Request-Id header available to the request and enables the ActionDispatch::Request#request_id method.
The unique request id can be used to trace a request end-to-end and would typically end up being part of log files from multiple pieces of the stack.
3.4.12. ActionDispatch::ServerTiming
ActionDispatch::ServerTiming sets a Server-Timing header containing performance metrics for the request.
3.4.13. ActionDispatch::Session::CookieStore
ActionDispatch::Session::CookieStore is responsible for storing the session in cookies.
3.4.14. ActionDispatch::ShowExceptions
ActionDispatch::ShowExceptions rescues any exception returned by the application and calls an exceptions app that will wrap it in a format for the end user.
3.4.15. ActionDispatch::Static
ActionDispatch::Static serves static files from the public folder. Disabled when config.public_file_server.enabled is false.
3.4.16. ActiveRecord::Migration::CheckPending
ActiveRecord::Migration::CheckPending checks pending migrations and raises ActiveRecord::PendingMigrationError if any migrations are pending if config.action_dispatch.x_sendfile_header is set to :page_load.
3.4.17. ActiveSupport::Cache::Strategy::LocalCache::Middleware
ActiveSupport::Cache::Strategy::LocalCache::Middleware is the middleware for the in-memory local cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread.
3.4.18. Propshaft::QuietAssets
Propshaft::QuietAssets suppresses logger output for asset requests.
3.4.19. Rack::ConditionalGet
Rack::ConditionalGet enables "Conditional GET" requests using if-none-match and if-modified-since. If the requested page wasn't changed returns a 304 Not Modified and an empty body.
3.4.20. Rack::ETag
Rack::ETag adds an ETag header on all String bodies. ETags are used to validate the cache to faciliate "Conditional GET" requests as described above. See the Caching with Rails for further information.
3.4.21. Rack::Head
Rack::Head returns an empty body for all HEAD requests. It leaves all other requests unchanged.
3.4.22. Rack::Lock
Rack::Lock locks every request inside a mutex, so that every request will effectively be executed synchronously.
3.4.23. Rack::MethodOverride
Rack::MethodOverride allows the method to be overridden if params[:_method] is set. This is how Rails supports PUT, PATCH, and DELETE HTTP methods since they are not browser native.
3.4.24. Rack::Runtime
Rack::Runtime sets an X-Runtime header, containing the time (in seconds) taken to execute the request.
3.4.25. Rack::Sendfile
Rack::Sendfile sets a server specific X-Sendfile header. This is useful for accelerated file sending if you use a reverse proxy server like Apache or Nginx. For example it can be set to 'X-Sendfile' for Apache. Configure this via config.action_dispatch.x_sendfile_header option.
3.4.26. Rack::TempfileReaper
Rack::TempfileReaper cleans up tempfiles used to buffer multipart requests.
3.4.27. Rails::Rack::Logger
Rails::Rack::Logger notifies the logs that the request has begun. After the request is complete, flushes all the logs.
You can use any of the above middleware in a custom Rack stack.
4. Custom Middleware
You can create your own middleware and include it in your Rails app.
4.1. Creating Middleware
Custom middleware files should be placed in the lib/ folder and required manually since middleware is not auto-reloaded.
The below example reads the locale value from the URL params and stores it in the Rack env. It then deletes it from the query parameters so it isn't included in the params hash keeping it decluttered when the request hits the controller.
# lib/middleware/extract_locale.rb
module RackMiddleware
class ExtractLocale
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
if request.params["locale"].present?
env["myapp.locale"] = env["action_dispatch.request.query_parameters"]["locale"]
env["action_dispatch.request.query_parameters"].delete("locale")
env["action_dispatch.request.parameters"].delete("locale")
end
@app.call(env)
end
end
end
Rails doesn't create the lib/middleware/ folder by default, so you'll need to create it yourself. Excluding it from the autoload path is recommended to prevent auto-loading issues.
# config/application.rb
module MyApp
class Application < Rails::Application
# ...
config.autoload_lib(ignore: %w[assets tasks middleware])
# ...
end
end
4.2. Adding Custom Middleware to the Stack
Custom middleware can be added in application.rb
# config/application.rb
# ...
require_relative "../lib/middleware/extract_locale"
module MyApp
class Application < Rails::Application
# ...
config.middleware.use RackMiddleware::ExtractLocale
# ...
end
end
or within a standalone initializer.
# config/initializers/extract_locale.rb
require "#{Rails.root.join("lib", "middleware", "extract_locale")}"
Rails.application.config.middleware.use RackMiddleware::ExtractLocale
5. Accessing Rack Internals in Rails
The underlying Rack API can be used within Rails controllers.
5.1. Accessing the Rack env
The Rack env hash is available in Rails controllers using request.env.
class HomeController
def index
user_agent = request.env["HTTP_USER_AGENT"]
# ...
end
end
5.2. Writing a Rack Response
A Rack response can be written in a Rails controller as:
class HomeController
def index
self.response = Rack::Response[200, {}, ["I'm Home!"]]
end
end
5.3. Routing to a Rack App
You can route requests to a Rack App in your config/routes.rb. See the routing guide for further details.