Rails と Rack

本ガイドでは、Railsと[Rack][]の統合について説明します。

このガイドの内容:

  • Rackとは何か、RailsでRackが使われている理由
  • RailsがRackミドルウェアでアプリケーションスタックを構築するしくみ
  • Action Packの内部ミドルウェアスタック
  • ミドルウェアスタックの設定と変更方法
  • Railsコントローラから利用できる、基盤となるRack API

1 Rackについて

Rackは、RubyでWebアプリケーションを開発するためのモジュール式インターフェイスを提供します。 HTTPリクエストとレスポンスをRackの規約に沿った構造でラップすることで、Webサーバー、Webフレームワーク、およびその間のソフトウェア(ミドルウェアと呼ばれます)のAPIを単一のメソッド呼び出しに統合します。

このしくみにより、PumaやFalconなどのRack準拠のWebサーバーを、RailsなどのRackベースのWebフレームワークで自由に差し替えられます。

RailsとRackの統合について詳しく知る前に、Rack自体について見てみましょう。

1.1 基本的なRackアプリケーション

Rackアプリケーションは、callメソッドを実装するオブジェクトです。callメソッドには、Rack環境として知られるenvハッシュを渡します。

以下は、最小限のRackアプリケーションの例です。

class App
  def call(env)
    [200, { "content-type" => "text/plain" }, ["Hello World"]]
  end
end

run App.new

HTTPリクエストを受け取ると、Rack準拠のWebサーバーがリクエストを解析してenvハッシュを作成し、このenvを渡してRackアプリケーションを呼び出します。 callメソッドが返す配列は、HTTPレスポンスを表す以下の3つの要素のみを含んでいなければなりません。

  1. HTTPレスポンスコード(上の例では200)
  2. 送信するHTTPレスポンスヘッダを含むハッシュ
  3. enumerableオブジェクト(レスポンスのbodyを表す文字列を返す)

Rackアプリケーションは、一般的にWebサーバーのコマンドラインプログラムで実行されます。 また、Rackアプリケーションのエントリポイントはconfig.ruファイルに格納されます。

$ 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

上のシェルコマンドを実行すると、Rackアプリが作成されてhttp://localhost:9292で起動します。 以下のコマンドで動作を確認できます。

$ curl localhost:9292
Hello World

1.2 Rackミドルウェア

Rackアプリケーションは、ミドルウェア(middleware)でラップできます。ミドルウェアは、リクエストがメインのアプリケーションに到達する直前と、メインのアプリケーションがリクエストに対してレスポンスを返した直後のどちらでも操作を実行できます。

ミドルウェアは通常、ログ出力、キャッシュ、認証、パフォーマンス計測などのタスクに利用されます。

Rackミドルウェアは、Rackアプリケーションと、ミドルウェアを設定するための任意の引数を受け取るnewメソッドを必ず持つ必要があります。newメソッドは、callに応答するRackアプリケーションを返す必要があります。

Rackミドルウェアはクラスとして書かれるのが普通で、ミドルウェアの各インスタンスは関連するアプリケーションへのアクセスをラップします。

class MyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # リクエストがメインのアプリケーションに到達する直前の操作はここで行う
    # -------------------------------------------------------

    # ミドルウェアスタックの下流にリクエストを伝播させる
    status, headers, body = @app.call(env)

    # ---------------------------------------
    # リクエストがアプリケーションから返された後の操作はここで行う

    # ミドルウェアスタックの上流にレスポンスを伝播させる
    [status, headers, body]
  end
end

ミドルウェアは、必要に応じて@app.callを完全にスキップして自分自身でレスポンスを返すことで、ミドルウェアスタックの処理を打ち切る(ショートサーキット)ことが可能です。この場合、リクエストはメインのアプリケーションやスタック内の残りのミドルウェアに到達しなくなります。

リクエスト認証用のミドルウェアは、ショートサーキットを利用する場合があります。

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

Rackアプリにミドルウェアを追加するには、useを使います。

class AuthenticateRequest
  # ...
end

class App
  def call(env)
    [200, { "content-type" => "text/plain" }, ["Hello World"]]
  end
end

use AuthenticateRequest
run App.new

Rackアプリケーションを構築するためのこのようなDSLは、Rack::Builderによって提供されます。Rackについて詳しくは、Rackの仕様およびRackのWebサイトを参照してください。

2 RailsとRack

2.1 Railsの主要なRackオブジェクト

Rails.applicationは、Railsアプリケーションにおける主要なRackアプリケーションオブジェクトです。 Rack準拠のWebサーバーは、Railsアプリケーションを提供するためにRails.applicationオブジェクトを使う必要があります。

2.2 Railsサーバーを起動する

Railsは、Rackup::Serverをサブクラス化する形でRails::Serverを作成します。 bin/rails serverコマンドを実行すると、Rails::Serverオブジェクトがインスタンス化されてWebサーバーが起動します。

Rails::Server.new.tap do |server|
  require APP_PATH
  Dir.chdir(Rails.application.root)
  server.start
end

サーバーの起動方法について詳しくは、Railsの初期化プロセスガイドを参照してください。

3 Action Dispatchのミドルウェアスタック

ActionDispatch::MiddlewareStackは、RailsのRack::Builderに相当します。Railsの要件を満たすために、より柔軟で多くの機能を備えています。

Rails::Applicationオブジェクトは、このActionDispatch::MiddlewareStackを使って内部ミドルウェアと外部ミドルウェアを組み合わせ、Railsを使った完全なRackアプリケーションを構築します。

3.1 ミドルウェアスタックを調べる

ミドルウェアスタックを表示するには、以下のコマンドを実行します。

$ bin/rails middleware

以下は、作成直後のRailsアプリケーションで表示されたミドルウェアスタックの例です。

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

上に示したデフォルトのミドルウェアの概要については、後述の内部ミドルウェアスタックを参照してください。

3.2 ミドルウェアスタックを設定する

Railsが提供するconfig.middleware設定インターフェイスを用いることで、ミドルウェアスタックのミドルウェアを追加・削除・変更できます。これはapplication.rb設定ファイルで行うことも、環境ごとのenvironments/<環境名>.rb設定ファイルで行うことも可能です。

3.2.1 ミドルウェアを追加する

ミドルウェアスタックに新しいミドルウェアを追加するには、以下の3つのメソッドが利用できます。

  • config.middleware.use(new_middleware, args): ミドルウェアスタックの末尾に新しいミドルウェアを追加します。

  • config.middleware.insert_before(existing_middleware, new_middleware, args): 新しいミドルウェアを、(第1引数で)指定された既存のミドルウェアの直前に追加します。

  • config.middleware.insert_after(existing_middleware, new_middleware, args): 新しいミドルウェアを、(第1引数で)指定された既存のミドルウェアの直後に追加します。

利用例:

# config/application.rb

# `Rack::BounceFavicon`を末尾に追加する
config.middleware.use Rack::BounceFavicon

# `ActionDispatch::Executor`の直後に`Lifo::Cache`を追加する。
# `Lifo::Cache`に`{ page_cache: false }`引数を渡す。
config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false
3.2.2 ミドルウェアを差し替える

config.middleware.swapを使って、ミドルウェアスタック内にあるミドルウェアを置き換えられます。

# config/application.rb

# ActionDispatch::ShowExceptionsをLifo::ShowExceptionsで置き換える
config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
3.2.3 ミドルウェアを移動する

ミドルウェアスタック内の既存のミドルウェアを移動して順序を変更するには、config.middleware.move_beforeやconfig.middleware.move_afterを使います。

# config/application.rb

# ActionDispatch::ShowExceptionsをLifo::ShowExceptionsの直前に移動する
config.middleware.move_before Lifo::ShowExceptions, ActionDispatch::ShowExceptions
# config/application.rb

# ActionDispatch::ShowExceptionsをLifo::ShowExceptionsの直後に移動する
config.middleware.move_after Lifo::ShowExceptions, ActionDispatch::ShowExceptions
3.2.4 ミドルウェアを削除する

config.middleware.deleteを使ってミドルウェアを削除します。

# config/application.rb
config.middleware.delete Rack::Runtime

delete!を使うと、指定したミドルウェアが存在しない場合にエラーが発生します。

# config/application.rb

config.middleware.delete! Some::NonExistentMiddleware

3.3 ミドルウェアスタックを再読み込みする

ミドルウェアスタックが一度読み込まれると、以後の変更は監視されません。 ミドルウェアスタックに変更を加えた場合は、サーバーを再起動してください。

3.4 内部ミドルウェアスタック

Action Controllerの機能の多くはミドルウェアとして実装されています。

それぞれのミドルウェアの目的について以下で説明します。

3.4.1 ActionDispatch::ActionableExceptions

ActionDispatch::ActionableExceptionsミドルウェアは、リクエストがローカルの場合に、Railsのエラーページからアクションをディスパッチする方法を提供します。

3.4.2 ActionDispatch::Callbacks

ActionDispatch::Callbacksミドルウェアは、リクエストのディスパッチ前後に実行されるコールバックを提供します。

3.4.3 ActionDispatch::ContentSecurityPolicy::Middleware

ActionDispatch::ContentSecurityPolicy::Middlewareミドルウェアは、Content-Security-Policyヘッダを設定するためのDSLを提供します。詳しくはセキュリティガイドを参照してください。

3.4.4 ActionDispatch::Cookies

ActionDispatch::Cookiesミドルウェアは、リクエストからCookieデータを読み取り、レスポンスにCookieデータを書き込みます。

3.4.5 ActionDispatch::DebugExceptions

ActionDispatch::DebugExceptionsミドルウェアは、例外をログに記録し、リクエストがローカルの場合にデバッグ用のページを表示します。

3.4.6 ActionDispatch::Executor

ActionDispatch::Executorミドルウェアは、development環境でコードがスレッドセーフで再読み込みされることを保証します。

3.4.7 ActionDispatch::Flash

ActionDispatch::Flashミドルウェアは、flashのキーを設定します。config.session_storeに何らかの値が設定されている場合にのみ利用可能です。

3.4.8 ActionDispatch::HostAuthorization

ActionDispatch::HostAuthorizationミドルウェアは、DNSリバインディング攻撃を防ぐために、リクエストの送信先として許可されるホストを制限します。設定方法については設定ガイドを参照してください。

3.4.9 ActionDispatch::Reloader

ActionDispatch::Reloaderミドルウェアは、development環境でのコード自動再読み込みを支援するために、prepareコールバックとcleanupコールバックを提供します。

3.4.10 ActionDispatch::RemoteIp

ActionDispatch::RemoteIpミドルウェアは、IPスプーフィング攻撃をチェックします。

3.4.11 ActionDispatch::RequestId

ActionDispatch::RequestIdミドルウェアは、リクエストに一意のX-Request-Idヘッダを設定し、ActionDispatch::Request#request_idメソッドを利用可能にします。

一意のリクエストIDは、リクエストのエンドツーエンドのトラッキングに利用できます。 通常は、スタックを構成する複数のコンポーネントのログファイルに記録されます。

3.4.12 ActionDispatch::ServerTiming

ActionDispatch::ServerTimingミドルウェアは、リクエストのパフォーマンス指標を含むServer-Timingヘッダを設定します。

3.4.13 ActionDispatch::Session::CookieStore

ActionDispatch::Session::CookieStoreミドルウェアは、セッションをCookieに保存する役割を担当します。

3.4.14 ActionDispatch::ShowExceptions

ActionDispatch::ShowExceptionsミドルウェアは、アプリケーションから返された例外をキャッチし、エンドユーザー向けの形式でラップする例外アプリを呼び出します。

3.4.15 ActionDispatch::Static

ActionDispatch::Staticミドルウェアは、publicフォルダの静的ファイルを配信します。 config.public_file_server.enabledがfalseの場合は無効化されます。

3.4.16 ActiveRecord::Migration::CheckPending

ActiveRecord::Migration::CheckPendingミドルウェアは、保留中のマイグレーションをチェックし、保留中のマイグレーションがある場合はActiveRecord::PendingMigrationErrorを発生させます。 config.active_record.migration_errorが:page_loadに設定されている場合にのみ有効です。

3.4.17 ActiveSupport::Cache::Strategy::LocalCache::Middleware

ActiveSupport::Cache::Strategy::LocalCache::Middlewareミドルウェアは、インメモリのローカルキャッシュ用のミドルウェアです。このキャッシュはスレッドセーフではなく、単一のスレッドの一時的なメモリキャッシュとしてのみ使われます。

3.4.18 Propshaft::QuietAssets

Propshaft::QuietAssetsミドルウェアは、アセットリクエストのログ出力を抑制します。

3.4.19 Rack::ConditionalGet

Rack::ConditionalGetミドルウェアは、if-none-matchおよびif-modified-sinceによる「条件付きGET」リクエストを処理します。 リクエストされたページが変更されていない場合、304 Not Modifiedを返し、bodyは空になります。

3.4.20 Rack::ETag

Rack::ETagミドルウェアは、すべての文字列bodyにETagヘッダを追加します。ETagはキャッシュのバリデーションに使われ、上記の「条件付きGET」リクエストで利用されます。 詳しくはキャッシュのガイドを参照してください。

3.4.21 Rack::Head

Rack::Headミドルウェアは、すべてのHEADリクエストに対して空のbodyを返します。それ以外のリクエストは変更されません。

3.4.22 Rack::Lock

Rack::Lockミドルウェアは、すべてのリクエストをミューテックス内でロックするため、すべてのリクエストは実質的に同期的に実行されます。

3.4.23 Rack::MethodOverride

Rack::MethodOverrideミドルウェアは、params[:_method]が設定されている場合にHTTPメソッドをオーバーライドできるようにします。これは、ブラウザがネイティブにサポートしていないPUT、PATCH、DELETE HTTPメソッドをRailsでサポートする方法です。

3.4.24 Rack::Runtime

Rack::Runtimeミドルウェアは、X-Runtimeヘッダを設定します。このヘッダには、リクエストの実行にかかった時間(秒単位)が含まれます。

3.4.25 Rack::Sendfile

Rack::Sendfileミドルウェアは、サーバー固有のX-Sendfileヘッダを設定します。

これは、ApacheやNginxのようなリバースプロキシサーバーを利用している場合に、ファイル送信を高速化するのに役立ちます。たとえば、Apacheの場合はX-Sendfileに設定できます。これはconfig.action_dispatch.x_sendfile_headerオプションで設定します。

3.4.26 Rack::TempfileReaper

Rack::TempfileReaperミドルウェアは、マルチパートリクエストをバッファリングするための一時ファイルをクリーンアップします。

3.4.27 Rails::Rack::Logger

Rails::Rack::Loggerミドルウェアは、リクエストの開始をログに通知します。リクエストが完了すると、すべてのログをフラッシュします。

これらのミドルウェアはいずれも、カスタムRackスタックで利用することも可能です。

4 カスタムミドルウェア

独自のミドルウェアを作成してRailsアプリに組み込むことも可能です。

4.1 ミドルウェアを作成する

カスタムミドルウェアは、lib/フォルダに配置したうえで、手動でrequireする必要があります(ミドルウェアは自動再読み込みされないため)。

以下の例では、URLパラメータからlocaleの値を読み取り、Rackのenvに保存してから、localeをクエリパラメータから削除します。 これにより、リクエストがRailsコントローラに到達したときにparamsハッシュにlocaleが含まれなくなり、パラメータをシンプルに保てます。

# 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は、lib/middleware/フォルダをデフォルトで作成しないため、自分で作成する必要があります。 自動読み込みの問題を避けるため、このフォルダはautoloadパスに含めないことが推奨されます。

# config/application.rb

module MyApp
  class Application < Rails::Application
    # ...

    config.autoload_lib(ignore: %w[assets tasks middleware])

    # ...
  end
end

4.2 カスタムミドルウェアをスタックに追加する

カスタムミドルウェアは、以下のようにconfig/application.rbファイルに追加できます。

# config/application.rb

# ...

require_relative "../lib/middleware/extract_locale"

module MyApp
  class Application < Rails::Application
    # ...

    config.middleware.use RackMiddleware::ExtractLocale

    # ...
  end
end

または、以下のように単独のイニシャライザにも追加できます。

# config/initializers/extract_locale.rb

require "#{Rails.root.join("lib", "middleware", "extract_locale")}"

Rails.application.config.middleware.use RackMiddleware::ExtractLocale

5 RailsでRackの内部にアクセスする

基盤となるRack APIは、Railsコントローラ内で利用できます。

5.1 Rackのenvにアクセスする

Railsのコントローラ内では、request.envでRackのenvハッシュにアクセスできます。

class HomeController
  def index
    user_agent = request.env["HTTP_USER_AGENT"]

    # ...
  end
end

5.2 Rackのレスポンスを書き込む

Railsのコントローラ内では、以下の方法でRackのレスポンスを直接書き込めます。

class HomeController
  def index
    self.response = Rack::Response[200, {}, ["I'm Home!"]]
  end
end

5.3 Rackアプリへのルーティング

config/routes.rbファイルでリクエストをRackアプリにルーティングできます。 詳しくはルーティングガイドを参照してください。

6 関連リンク

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイド協賛プラン - バナー画像

Railsガイドは下記の協賛企業から継続的な支援を受けています。もしご興味あれば、協賛プランから気軽にお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加