More at rubyonrails.org:

Rails on Rack

This guide covers Rails' integration with Rack. After reading this guide, you will know:

  • What Rack is and why Rails uses it.
  • How Rails uses Rack middleware to build an application stack.
  • Action Pack's internal middleware stack.
  • How to configure and change the middleware stack.
  • The underlying Rack API exposed by Rails controllers.

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:

  1. The HTTP response code (200 in the above example).
  2. A hash containing any HTTP response headers we wish to send.
  3. 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.



Back to top