Rails Rackミドルウェア入門
リクエスト処理の仕組みを理解する
RackミドルウェアのアーキテクチャとRailsでの活用方法を解説。カスタムミドルウェアの作成からロギング・CORS対応まで学べます。
こんな人向けの記事です
- Railsのリクエスト処理の仕組みを理解したい
- Rackミドルウェアとは何か知りたい
- カスタムミドルウェアを作成したい
Step 1Rackとは何か
Rackは、RubyのWebサーバーとWebアプリケーションフレームワークをつなぐインターフェース仕様です。Puma、Unicornなどのサーバーと、Rails、Sinatraなどのフレームワークが、共通のプロトコルで通信できるようにします。
Rackの本質は「callメソッドを持つオブジェクト」というシンプルな規約です。引数に環境変数のハッシュ(env)を受け取り、[ステータスコード, ヘッダー, ボディ]の3要素の配列を返します。
最もシンプルなRackアプリケーションは以下のとおりです。
# 最小のRackアプリケーション
app = Proc.new do |env|
[200, { "Content-Type" => "text/plain" }, ["Hello, Rack!"]]
end
run app
このファイルをconfig.ruとして保存し、rackupコマンドで起動できます。
$ gem install rack
$ rackup config.ru
# => Puma starting on http://localhost:9292
Rackの役割を図で表すと次のようになります。
| 層 | 役割 | 例 |
|---|---|---|
| Webサーバー | HTTPリクエストの受信・レスポンスの送信 | Puma, Unicorn, Falcon |
| Rack | サーバーとアプリの橋渡し(インターフェース) | Rack仕様 |
| フレームワーク | ルーティング・コントローラー・ビュー処理 | Rails, Sinatra, Hanami |
Step 2Rackミドルウェアの仕組み
Rackミドルウェアは、リクエストとレスポンスの間に挟まるフィルターのような存在です。複数のミドルウェアが順番に処理を行い、最終的にアプリケーション本体に到達します。
ミドルウェアは「玉ねぎの層」に例えられます。リクエストは外側から内側へ、レスポンスは内側から外側へ通過します。各層が独立した処理を担当します。
ミドルウェアの基本構造は以下のとおりです。
class ExampleMiddleware
def initialize(app)
@app = app # 次のミドルウェアまたはアプリ本体
end
def call(env)
# === リクエスト処理(前処理) ===
# ここでリクエストを加工・検査できる
status, headers, body = @app.call(env) # 次に処理を渡す
# === レスポンス処理(後処理) ===
# ここでレスポンスを加工できる
[status, headers, body]
end
end
ミドルウェアチェーンの処理の流れを整理します。
リクエスト
│
▼
[ミドルウェアA] 前処理 → @app.call(env)
│ │
│ ▼
│ [ミドルウェアB] 前処理 → @app.call(env)
│ │ │
│ │ ▼
│ │ [Railsアプリ本体]
│ │ │
│ │ ▼
│ [ミドルウェアB] 後処理 ← レスポンス
│ │
▼
[ミドルウェアA] 後処理 ← レスポンス
│
▼
レスポンス
Step 3Railsのミドルウェアスタック
Railsはデフォルトで多くのミドルウェアを組み込んでいます。現在のミドルウェアスタックは以下のコマンドで確認できます。
$ bin/rails middleware
代表的なRailsミドルウェアの一覧です。
| ミドルウェア | 役割 |
|---|---|
ActionDispatch::HostAuthorization |
許可されたホストからのリクエストのみ通す |
Rack::Sendfile |
静的ファイルの効率的な配信 |
ActionDispatch::Executor |
スレッドセーフなリクエスト処理 |
ActiveSupport::Cache::Strategy::LocalCache::Middleware |
リクエストごとのキャッシュ管理 |
Rack::Runtime |
X-Runtimeヘッダーの付与(処理時間) |
ActionDispatch::RequestId |
リクエストにユニークIDを付与 |
Rails::Rack::Logger |
リクエストのログ出力 |
ActionDispatch::ShowExceptions |
例外のHTMLレスポンス変換 |
ActionDispatch::Cookies |
Cookie の読み書き |
ActionDispatch::Session::CookieStore |
セッション管理 |
ActionDispatch::Flash |
Flashメッセージ |
Rack::Head |
HEADリクエストへの対応 |
Rack::ConditionalGet |
条件付きGET(304レスポンス) |
Rack::ETag |
ETagヘッダーの自動付与 |
注意: Railsのバージョンによってデフォルトのミドルウェア構成は変わります。bin/rails middlewareで必ず自分の環境を確認してください。
Step 4カスタムミドルウェアの作成
独自のミドルウェアを作成してみましょう。まず、リクエストの処理時間を計測するシンプルなミドルウェアを作ります。
class RequestTimer
def initialize(app)
@app = app
end
def call(env)
start_time = Time.now
status, headers, body = @app.call(env)
duration = ((Time.now - start_time) * 1000).round(2)
headers["X-Request-Duration"] = "#{duration}ms"
Rails.logger.info "[RequestTimer] #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]} - #{duration}ms"
[status, headers, body]
end
end
作成したミドルウェアをRailsに登録します。
require_relative "../lib/middleware/request_timer"
module MyApp
class Application < Rails::Application
# ミドルウェアスタックの末尾に追加
config.middleware.use RequestTimer
end
end
次に、特定のIPアドレスをブロックするミドルウェアの例です。
class IpBlocker
BLOCKED_IPS = %w[192.168.1.100 10.0.0.50].freeze
def initialize(app)
@app = app
end
def call(env)
client_ip = env["REMOTE_ADDR"]
if BLOCKED_IPS.include?(client_ip)
[403, { "Content-Type" => "text/plain" }, ["Forbidden"]]
else
@app.call(env)
end
end
end
ミドルウェア内で@app.call(env)を呼ばずに直接レスポンスを返すと、それ以降のミドルウェアやアプリ本体は実行されません。認証やアクセス制御に便利なパターンです。
Step 5ミドルウェアの追加・削除・順序変更
Railsではconfig.middlewareを使ってミドルウェアスタックを柔軟に操作できます。
追加方法
# スタックの末尾に追加
config.middleware.use MyMiddleware
# 特定のミドルウェアの前に追加
config.middleware.insert_before ActionDispatch::Cookies, MyMiddleware
# 特定のミドルウェアの後に追加
config.middleware.insert_after Rails::Rack::Logger, MyMiddleware
削除方法
# ミドルウェアを削除
config.middleware.delete ActionDispatch::Flash
# ミドルウェアを入れ替え
config.middleware.swap ActionDispatch::Flash, MyCustomFlash
順序変更
# 先頭に移動(最初に実行される)
config.middleware.move_before 0, MyMiddleware
# 特定のミドルウェアの前に移動
config.middleware.move_before ActionDispatch::Cookies, MyMiddleware
# 特定のミドルウェアの後に移動
config.middleware.move_after Rails::Rack::Logger, MyMiddleware
各メソッドの使い分けを整理します。
| メソッド | 用途 |
|---|---|
use |
スタックの末尾に追加 |
insert_before |
指定ミドルウェアの前に追加 |
insert_after |
指定ミドルウェアの後に追加 |
delete |
指定ミドルウェアを削除 |
swap |
指定ミドルウェアを別のものに入れ替え |
move_before |
指定位置の前に移動 |
move_after |
指定位置の後に移動 |
注意: セッションやCookieなど、他のミドルウェアに依存するものを削除すると動作しなくなる場合があります。削除する前に依存関係を確認してください。
Step 6実践例(ロギング・認証・CORS対応)
1. 詳細なリクエストロギング
リクエストとレスポンスの詳細をJSON形式でログに出力するミドルウェアです。
class JsonLogger
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
start_time = Time.now
status, headers, body = @app.call(env)
duration = ((Time.now - start_time) * 1000).round(2)
log_data = {
timestamp: Time.now.iso8601,
method: request.request_method,
path: request.path,
query: request.query_string,
status: status,
duration_ms: duration,
ip: request.ip,
user_agent: request.user_agent
}
Rails.logger.info log_data.to_json
[status, headers, body]
end
end
2. APIトークン認証
API用のトークン認証を行うミドルウェアです。/apiパスへのリクエストにのみ適用します。
class ApiTokenAuth
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# /api パス以外はスルー
unless request.path.start_with?("/api")
return @app.call(env)
end
token = env["HTTP_AUTHORIZATION"]&.gsub(/^Bearer /, "")
if token.nil? || token.empty?
return [
401,
{ "Content-Type" => "application/json" },
[{ error: "Authorization token required" }.to_json]
]
end
unless valid_token?(token)
return [
403,
{ "Content-Type" => "application/json" },
[{ error: "Invalid token" }.to_json]
]
end
@app.call(env)
end
private
def valid_token?(token)
# 実際にはDBやRedisでトークンを検証
ApiToken.exists?(token: token, active: true)
end
end
3. CORS対応
フロントエンドとバックエンドを分離している場合に必要なCORS(Cross-Origin Resource Sharing)対応のミドルウェアです。
class CorsHandler
ALLOWED_ORIGINS = %w[
http://localhost:3001
https://frontend.example.com
].freeze
def initialize(app)
@app = app
end
def call(env)
origin = env["HTTP_ORIGIN"]
# プリフライトリクエスト(OPTIONS)への対応
if env["REQUEST_METHOD"] == "OPTIONS"
return preflight_response(origin)
end
status, headers, body = @app.call(env)
if ALLOWED_ORIGINS.include?(origin)
headers["Access-Control-Allow-Origin"] = origin
headers["Access-Control-Allow-Credentials"] = "true"
end
[status, headers, body]
end
private
def preflight_response(origin)
headers = {
"Access-Control-Allow-Origin" => origin,
"Access-Control-Allow-Methods" => "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers" => "Content-Type, Authorization, X-Requested-With",
"Access-Control-Max-Age" => "86400"
}
[204, headers, []]
end
end
これらのミドルウェアを登録する例です。
require_relative "../lib/middleware/json_logger"
require_relative "../lib/middleware/api_token_auth"
require_relative "../lib/middleware/cors_handler"
module MyApp
class Application < Rails::Application
# CORSは最も外側(最初)に配置
config.middleware.insert_before 0, CorsHandler
# ロギングはRails::Rack::Loggerの後に配置
config.middleware.insert_after Rails::Rack::Logger, JsonLogger
# API認証はルーティングの前に配置
config.middleware.use ApiTokenAuth
end
end
本番環境でのCORS対応にはrack-cors gemの利用も検討してください。Gemfileにgem "rack-cors"を追加するだけで、より堅牢なCORS設定が可能になります。
まとめ
この記事で学んだこと
- RackはWebサーバーとアプリケーションを繋ぐインターフェース仕様
- ミドルウェアは
initialize(app)とcall(env)を持つシンプルなクラス - Railsにはデフォルトで多数のミドルウェアが組み込まれている
config.middlewareで追加・削除・順序変更が可能- ロギング、認証、CORS対応など横断的な処理に最適
- ミドルウェアの配置順序がアプリケーションの動作に影響する