Skip to content

Rackミドルウェアを自作してリクエスト/レスポンスをカスタマイズする

はじめに

Railsアプリケーションは、ブラウザからのリクエストを受け取ってから、コントローラのアクションが実行されるまでの間に、数多くの処理を行っています。セッション管理、クッキーの解析、パラメータのパース、キャッシュ処理など、これらの機能の多くはRackミドルウェアとして実装されています。

Rackとは、RubyのWebサーバーとWebフレームワーク(Rails, Sinatraなど)の間の標準的なインターフェースを定義した仕様です。そしてRackミドルウェアは、リクエストとレスポンスの間に挟まり、それらを検査したり変更したりできるコンポーネントです。Railsの心臓部とも言えるこの仕組みを理解し、自作できるようになることで、アプリケーションの動作をより低レベルで制御することが可能になります。

この記事では、Rackミドルウェアの基本的な概念を解説し、実際にカスタムミドルウェアを作成してRailsアプリケーションに組み込む方法を紹介します。

Rackの基本

Rackの最も基本的なコンセプトは、「callメソッドを持つオブジェクト」です。このオブジェクトは、環境変数(リクエスト情報)のハッシュ(env)を引数として受け取り、[ステータスコード, ヘッダー, レスポンスボディ]という3つの要素からなる配列を返します。

ruby
class HelloWorldApp
  def call(env)
    status = 200
    headers = { "Content-Type" => "text/plain" }
    body = ["Hello, Rack!"]

    [status, headers, body]
  end
end

Railsアプリケーション自体も、このRack仕様に準拠した巨大なアプリケーションオブジェクトと見なすことができます。

Rackミドルウェアとは?

Rackミドルウェアは、このRackアプリケーションをラップする(包み込む)オブジェクトです。ミドルウェアもまたcallメソッドを持ちますが、その中で次のミドルウェアまたはアプリケーション本体のcallメソッドを呼び出す点が異なります。

ruby
class MyMiddleware
  def initialize(app)
    @app = app # 次に実行すべきミドルウェア or アプリケーション本体
  end

  def call(env)
    # --- リクエストに対する前処理 --- 
    # envハッシュを変更したり、リクエストを記録したりできる

    # 次のミドルウェア/アプリを呼び出し、レスポンスを受け取る
    status, headers, body = @app.call(env)

    # --- レスポンスに対する後処理 ---
    # status, headers, bodyを変更したりできる

    # 最終的なレスポンスを返す
    [status, headers, body]
  end
end

このように、ミドルウェアは玉ねぎの皮のように層をなしており、リクエストは外側から内側へ、レスポンスは内側から外側へと伝わっていきます。rails middlewareコマンドを実行すると、現在のアプリケーションで使われているミドルウェアのスタック(層の順番)を確認できます。

カスタムミドルウェアの作成

それでは、実際に簡単なカスタムミドルウェアを作成してみましょう。ここでは、すべてのレスポンスにカスタムヘッダー(例: X-App-Version: 1.0)を追加するミドルウェアを作成します。

1. ミドルウェアクラスの作成

lib/middlewareディレクトリを作成し、その中にミドルウェアのファイルを作成するのが一般的です。

bash
mkdir -p lib/middleware
touch lib/middleware/version_header.rb
ruby
# lib/middleware/version_header.rb
module Middleware
  class VersionHeader
    def initialize(app, version: "1.0")
      @app = app
      @version = version
    end

    def call(env)
      # 次のミドルウェアを呼び出してレスポンスを取得
      status, headers, body = @app.call(env)

      # レスポンスヘッダーにカスタムヘッダーを追加
      headers['X-App-Version'] = @version

      # 変更後のレスポンスを返す
      [status, headers, body]
    end
  end
end
  • initializeメソッドで、次のアプリケーション@appと、オプションの引数(ここではバージョン番号)を受け取れるようにしています。

2. ミドルウェアの読み込みと登録

作成したミドルウェアをRailsアプリケーションに認識させる必要があります。

まず、libディレクトリ配下のファイルが自動で読み込まれるように、config/application.rbに設定を追加します。

ruby
# config/application.rb
module YourApp
  class Application < Rails::Application
    # ...
    config.autoload_paths << Rails.root.join('lib')
  end
end

次に、同じくconfig/application.rbで、ミドルウェアスタックに自作のミドルウェアを追加します。

ruby
# config/application.rb
# ...
require_relative '../lib/middleware/version_header' # ファイルを明示的にrequire

module YourApp
  class Application < Rails::Application
    # ...
    config.autoload_paths << Rails.root.join('lib')

    # ミドルウェアスタックに追加
    config.middleware.use Middleware::VersionHeader, version: "1.1"
  end
end
  • config.middleware.use: ミドルウェアをスタックの末尾に追加します。
  • Middleware::VersionHeaderの後ろに渡した引数(version: "1.1")は、ミドルウェアのinitializeメソッドに渡されます。
  • insert_beforeinsert_afterを使えば、スタック内の特定の位置にミドルウェアを挿入することもできます。

3. 動作確認

Railsサーバーを再起動し、ブラウザの開発者ツールやcurlコマンドでレスポンスヘッダーを確認してみましょう。

bash
curl -I http://localhost:3000

レスポンスの中にX-App-Version: 1.1というヘッダーが含まれていれば成功です。

カスタムミドルウェアのユースケース

カスタムミドルウェアは、様々な場面で活用できます。

  • APIキーによる認証: 特定のパス(例: /api/以下)へのリクエストヘッダーをチェックし、有効なAPIキーが含まれていなければ401エラーを返す。
  • メンテナンスモード: 特定の条件下で、すべてのリクエストをメンテナンスページにリダイレクトする。
  • リクエストのロギング: 特定の条件に合致するリクエストの詳細な情報を、Railsのログとは別のファイルに記録する。
  • 下位互換性のためのパラメータ変換: 古いAPIクライアントからのリクエストに含まれるパラメータ名を、新しい形式に変換してからコントローラに渡す。
  • サイト全体のA/Bテスト: リクエストに応じて、異なるバージョンのアプリケーションロジックに処理を振り分ける。

まとめ

Rackミドルウェアは、Railsアプリケーションのリクエスト/レスポンスサイクルに介入するための強力なフックです。普段意識することは少ないかもしれませんが、Railsの根幹を支えるこの仕組みを理解することで、フレームワークの提供する機能だけでは実現が難しい、より高度な要求に柔軟に対応できるようになります。

コントローラよりも低いレイヤーで、アプリケーション全体に影響を与えるような横断的な関心事(cross-cutting concern)を処理したい場合に、カスタムミドルウェアの作成は非常に有効な選択肢となるでしょう。

AI が自動生成した技術記事をまとめたテックブログ