Ruby

Rackとは?Rubyのサーバーとアプリをつなぐ仕様とミドルウェアの書き方【Rack 3.2】

Rackとは?Rubyのサーバーとアプリをつなぐ仕様とミドルウェアの書き方【Rack 3.2】

Rack(ラック)は、RubyのWebサーバーとWebアプリケーションのあいだの受け渡し方を決めた仕様と、その仕様を実装したgemの名前です。Rails、Sinatra、Hanami、Rodaはどれも Rack アプリケーションとして動き、Puma や Unicorn はどれも Rack アプリケーションを動かせます。この記事では、仕様の中身(call と3要素の配列、env)、config.ru と rackup、ミドルウェアの書き方、Rails との関係、Rack 2 から 3 で変わった点を、2026年9月時点の最新版 Rack 3.2.7 で実際に動かした結果とともに説明します。

なお、サーバーを収める19インチラックなどの「ラック」とは別物です。この記事が扱うのは Ruby の Rack です。

まとめ:Rackの仕様・最新版・Rack 3の要点

  • 定義:call(env) に応答し、[ステータス, ヘッダ, ボディ] の配列を返すオブジェクトが Rack アプリケーション。サーバーはこの形だけを前提に呼び出す
  • 最新版:rack 3.2.7(2026-08-13)。3.1系はセキュリティ修正のみ、3.0系はサポート終了、2.2系はセキュリティ修正のみ
  • Rack 3 の必須対応:レスポンスヘッダ名は小文字(content-type)。Content-Type と書くと Rack::Lint が例外を送出し、既定の rackup 開発環境では500応答になる
  • rackup は別gem:Rack 3.0 で rackup コマンドと Rack::Handler が rack から切り出された。本番は puma などサーバーのコマンドで直接起動するのが推奨
  • ミドルウェア:@app.call(env) の前後に処理を挟むクラス。ボディに対して each を直接呼ぶのは Rack 3 の仕様違反
  • Rails との関係:Rails アプリ自体が1つの Rack アプリケーションで、bin/rails middleware で積まれたミドルウェアを確認できる

Rackアプリケーションの定義:callと3要素の配列

Rack の仕様書(SPEC.rdoc)は、Rack アプリケーションを「call に応答する Ruby オブジェクト」と定義しています。call は引数を1つ(env=リクエストを表すHash)だけ受け取り、凍結されていない3要素の配列を返します。3要素は順に、HTTPステータス(100以上の整数)、ヘッダ(Hash)、ボディ(each か call に応答するオブジェクト)です。

call に応答すればよいので、lambda でも Rack アプリケーションになります。

app = ->(env) do
  [200, { "content-type" => "text/plain" }, ["Hello Rack\n"]]
end

サーバー側は、受け取ったHTTPリクエストを env に詰めて app.call(env) を呼び、返ってきた配列をHTTPレスポンスに組み立て直すだけです。フレームワークが何であっても、この入口と出口の形が同じなので、サーバーとフレームワークを自由に組み合わせられます。

誕生の経緯とPythonのWSGIとの関係

Rack の最初のリリース 0.1 は2007年3月3日で、作者は Leah Neukirchen(旧記事などでは旧名の Christian Neukirchen と書かれていることがあります)です。当時は Mongrel、WEBrick、FastCGI といったサーバーごとにフレームワーク側が個別のアダプタを書いており、Rack はこの組み合わせの数だけ必要だった接続コードを1つの約束事にまとめました。

README の謝辞には「Rack が土台にしている、よく作り込まれ文書化された成果」として WSGI チームの名前があります。Python の WSGI(同期)や ASGI(非同期)が Rack と同じ役割のインターフェースで、Python で FastAPI を ASGI サーバーの Uvicorn で動かす構図は、Ruby で Rails を Rack サーバーの Puma で動かす構図に対応します。

env(リクエスト環境)に入っている値

env は文字列キーのHashで、CGI 由来のキーと rack. で始まる Rack 固有のキーが入ります。Rack 3.2 からはキーが文字列であることが仕様上の必須になりました。主なキーは次のとおりです。

キー 内容 例
REQUEST_METHOD HTTPメソッド(必須) GET
PATH_INFO アプリ内でのパス /users
QUERY_STRING ?以降(空でも必須) id=1
SCRIPT_NAME アプリがマウントされた位置 /api
SERVER_PROTOCOL HTTPバージョン(3.0で必須化) HTTP/1.1
HTTP_* リクエストヘッダ HTTP_USER_AGENT
rack.url_scheme http・https・ws・wss https
rack.input リクエストボディ(3.1で任意化) IO風オブジェクト
rack.errors エラー出力先 $stderr相当

env を直接読むのは手間なので、実際には Rack::Request.new(env) で包んで req.path_info や req.params を使います。map "/api" の下にマウントしたアプリへ /api/users?id=1 を送ると、SCRIPT_NAME が /api、PATH_INFO が /users に分かれて届きます(次章のコードで確認できます)。

rack.input は Rack 3.1 で「あれば入力ストリーム」という任意項目に変わりました。ボディの無い GET だけを受けるサーバーは渡さなくてもよいため、自作ミドルウェアで env["rack.input"].read と書くと nil で落ちることがあります。Rack::Request#body も入力ストリームがなければ nil を返すため、取得方法にかかわらず nil を確認してから読みます。

config.ruとRack::Builder:use・map・runの役割

Rack アプリケーションの構成は、慣例として config.ru というファイルに書きます。中身は Rack::Builder の記法(Rack DSL)で、use がミドルウェアの追加、map がパスごとの振り分け、run が最後に呼ぶアプリケーションの指定です。

# config.ru
require "json"

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

  def call(env)
    started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    status, headers, body = @app.call(env)
    elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
    headers["x-runtime-ms"] = format("%.2f", elapsed * 1000)
    [status, headers, body]
  end
end

use Rack::Static, urls: ["/public"]
use Rack::Deflater
use Timing

map "/api" do
  run ->(env) {
    req = Rack::Request.new(env)
    [200, { "content-type" => "application/json" }, [JSON.generate(path: req.path_info, q: req.params)]]
  }
end

run ->(env) { [200, { "content-type" => "text/plain" }, ["Hello Rack\n" * 200]] }

use は書いた順に外側から積まれ、リクエストは上から下へ、レスポンスは下から上へ通ります。上の例では Rack::Static が最も外側にあるため、/public 配下の静的ファイルは Rack::Deflater を通らず、圧縮されずに返ります。圧縮もかけたいなら Rack::Deflater を先に書きます。この順序の違いは挙動の差としてそのまま出るので、use の並びは意図して決めてください。

Puma 8.0.2 で起動して確かめた結果は次のとおりです。

$ gem install rack -v 3.2.7
$ gem install puma -v 8.0.2
$ puma -p 9393 config.ru

$ curl -i "localhost:9393/api/users?id=1"
HTTP/1.1 200 OK
content-type: application/json
x-runtime-ms: 0.37
vary: Accept-Encoding
content-length: 32

{"path":"/users","q":{"id":"1"}}

rackupコマンドとpumaコマンドの使い分け

Rack 3.0(2022-09-06)で、rackup コマンド、Rack::Server、Rack::Handler は rack から別gemの rackup へ切り出されました。rack だけの環境で rackup が「command not found」になる、古いコードの Rack::Handler::WEBrick が uninitialized constant Rack::Handler で落ちる、という症状はこれが原因です。Rackup::Handler に読み替えるか、サーバーのコマンドで起動します。

rackup gem の README は、自身を「本番用に設計されていない」と明記しています。現行の rackup 2.3.1 は WEBrick に依存しておらず、メンテナの推奨は「フレームワークは config.ru を用意し、利用者は puma や falcon のコマンドで直接起動する」です。

もう1つの違いは、rackup が環境ごとに自動で挟むミドルウェアです。rackup は既定の development 環境で Rack::CommonLogger・Rack::ShowExceptions・Rack::Lint を積みます。そのため同じ config.ru でも、次の結果になりました(rack 3.2.7、rackup 2.3.1、puma 8.0.2 で実行)。

起動方法 ヘッダ名 Content-Type ヘッダ名 content-type
rackup(development) 500(LintError) 200
rackup -E production 200 200
puma config.ru 200 200

本番相当の起動では大文字ヘッダも通ってしまうため、仕様違反に気づくのは開発時の rackup か、テストで Rack::Lint を挟んだときだけです。自作のアプリやミドルウェアは、次章の方法で Lint を通るか確かめておくべきです。

Rackミドルウェアの書き方とRack 3の制約

ミドルウェアは、次のアプリ(@app)を受け取って保持し、自分の call の中で @app.call(env) を呼ぶクラスです。呼ぶ前に env を書き換えればリクエストの前処理、呼んだ後に3要素を書き換えればレスポンスの後処理になります。@app.call を呼ばずに自分でレスポンスを返せば、認証失敗時の401のように処理をその場で打ち切れます。

前章の Timing がこの基本形です。計測対象は下流アプリの呼び出しから戻るまでで、ボディの列挙・送信完了までの時間は含みません。ヘッダを足すときは、キーを必ず小文字で書きます。Rack 3 の SPEC は「ヘッダのキーに大文字のASCII文字を含めてはならない」と定めています。

ボディ加工の制約:eachの直接呼び出し禁止

ボディを書き換えるミドルウェアで避けるべきなのが、受け取ったボディに each を呼んで文字列を集める書き方です。Rack 3 の SPEC は「ミドルウェアはボディの each を直接呼んではならない。元のボディの each を呼ぶ新しいボディを返す」と定めており、Rack::Lint を挟むと次のように検出されます。

# 仕様違反の例
class BadUpcase
  def initialize(app) = @app = app

  def call(env)
    status, headers, body = @app.call(env)
    text = +""
    body.each { |chunk| text << chunk }
    [status, headers, [text.upcase]]
  end
end
# => Rack::Lint::LintError: Middleware must not call #each directly

正しい形は、元のボディを包むボディクラスを返し、close を元のボディへ伝えることです。SPEC は、ボディを差し替えた場合も元のボディの close を呼ぶよう求めています。長さが変わるので content-length も外します。

class Upcase
  def initialize(app) = @app = app

  def call(env)
    status, headers, body = @app.call(env)
    headers.delete("content-length")
    [status, headers, UpcaseBody.new(body)]
  end

  class UpcaseBody
    def initialize(body) = @body = body

    def each
      @body.each { |chunk| yield chunk.upcase }
    end

    def close
      @body.close if @body.respond_to?(:close)
    end
  end
end

ボディ全体を一度に読みたい場合、SPEC が許しているのは、ボディが to_ary に応答するときに限って to_ary を呼ぶ方法だけです。なお上の UpcaseBody が扱えるのは each に応答する Enumerable Body で、call 型の Streaming Body(Proc など)を渡すと undefined method 'each' で落ちます。Streaming Body を返すアプリの前に置くなら、respond_to?(:each) で判定して加工せず返すか、ストリームへの書き込みを変換する別実装が必要です。

Rack::LintとMockRequestによる仕様適合テスト

サーバーを起動しなくても、Rack::MockRequest でリクエストを模擬できます。ミドルウェアの内側と外側の両方に Rack::Lint を挟むと、受け取る側・返す側の両方の仕様違反を検出できます。

require "rack"
require "rack/mock_request"

inner = ->(env) { [200, { "content-type" => "text/plain", "content-length" => "5" }, ["hello"]] }
app = Rack::Lint.new(Upcase.new(Rack::Lint.new(inner)))

res = Rack::MockRequest.new(app).get("/")
puts res.status  # => 200
puts res.body    # => HELLO

Rails や Sinatra のアプリ全体をテストするなら、同じ仕組みの上に作られた rack-test gem(現行 2.2.0)が使えます。Rails の結合テストも内部で rack-test を使っています。

Rackに同梱されている主なミドルウェア

rack gem 自体にも、よく使うミドルウェアが入っています。rack 3.2.7 の lib/rack.rb で autoload されているもののうち、使う機会の多いものを挙げます。

ミドルウェア 役割 注意点
Rack::Deflater レスポンスのgzip圧縮 deflate方式には非対応
Rack::Static 指定パス配下の静的ファイル配信 3.2.6で公開範囲の脆弱性を修正
Rack::Directory ディレクトリ一覧の表示 本番公開は避ける
Rack::Files ファイル配信(旧Rack::File) Rack::Fileは3.1で削除
Rack::ETag / ConditionalGet ETag付与と304応答 Railsの既定スタックに入る
Rack::MethodOverride _methodでPUT・DELETEを送る POSTのフォーム用
Rack::Runtime x-runtimeヘッダで処理時間 Railsの既定スタックに入る
Rack::Lint SPEC違反の検出 開発・テスト用

Rack::Deflater はソースのコメントに「名前に反して deflate エンコーディングには対応しない」とあり、対応するのは gzip と identity だけです。if:(条件で圧縮を切り替えるlambda)、include:(圧縮対象のContent-Type)、sync:(チャンクごとにフラッシュするか、既定 true)の3つのオプションを持ちます。前章の config.ru で同じ文を200回繰り返した2,200バイトのレスポンスは、Accept-Encoding: gzip 付きで55バイトになりました(繰り返しの多い本文なので、実際のHTMLでここまでは縮みません)。ブラウザとの圧縮方式の交渉は Accept-Encodingの解説で扱っています。nginx や CDN で圧縮しているなら、アプリ側で二重にかける必要はありません。

Rack::Static と Rack::Directory は、2026-04-01 の rack 3.2.6(3.1.21、2.2系も同日)で脆弱性がまとめて修正されました。Rack::Directory のルートディレクトリ漏えい(CVE-2026-34763)と、Rack::Static の前方一致で意図しないファイルが見える問題(CVE-2026-34785)です。どちらも公開サーバーで使っているなら、3.2.6 以降へ上げることが先決です。Rack::Directory は一覧をそのまま外に見せる機能なので、本番の公開パスには置かないでください。

RailsとRackの関係:ミドルウェアスタックの確認と追加

Rails アプリケーションは、それ自体が1つの Rack アプリケーションです。新規生成した Rails アプリの config.ru には run Rails.application が書かれていて、Puma は Rails.application.call(env) を呼びます。Rails 8.1 の Gemfile ジェネレータは Puma を既定で入れ、その前段に置く Thruster も既定で追加します。

Rails は Rack::Builder の代わりに ActionDispatch::MiddlewareStack でミドルウェアを積みます。積まれている中身は次のコマンドで確認できます。

$ bin/rails middleware
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use Propshaft::Server
use ActionDispatch::Executor
...
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run MyApp::Application.routes

Rails ガイド(Rails on Rack、v8.1.4)が載せている新規アプリの例では28個が積まれ、そのうち Rack::Sendfile・Rack::Runtime・Rack::MethodOverride・Rack::Head・Rack::ConditionalGet・Rack::ETag・Rack::TempfileReaper の7個が rack gem 由来です。自作やgemのミドルウェアは config/application.rb で追加します。

# config/application.rb
config.middleware.use Timing                                  # 最後尾に追加
config.middleware.insert_before Rack::Runtime, Timing          # 指定の前に追加
config.middleware.insert_after ActionDispatch::Executor, Timing # 指定の後に追加
config.middleware.swap ActionDispatch::ShowExceptions, MyShowExceptions
config.middleware.delete Rack::Runtime                         # x-runtimeを出さない

delete は対象が無くても黙って通り、delete! は対象が無ければ例外を出します。Rails API モードではスタックが小さくなり、Cookie やセッションのミドルウェアは入りません。API モードでセッションを使いたいときに config.middleware.use ActionDispatch::Cookies などを自分で足すのはこのためです。

Rails 8.1.4(2026-09-24)の actionpack は rack >= 2.2.4 を要求しており、Rack 3 は必須ではありません。古いアプリを Rails だけ上げた場合、Gemfile.lock の rack が 2.2 系のまま残っていることがあります。bundle exec ruby -e 'require "rack"; puts Rack.release' で実際の版を確認できます。

Rack 2からRack 3で変わった点と移行時に壊れる箇所

Rack 3.0 は2022-09-06、3.1 は2024-06-11、3.2 は2025-07-31 にリリースされました。Rack 2 時代の記事やgemのコードをそのまま使うと、次の箇所で動かなくなるか警告が出ます。

変更 版 対処
レスポンスヘッダ名の大文字禁止 3.0 content-type 等の小文字で書く
rackup・Rack::Handler を別gemへ 3.0 gem rackup を追加、Rackup::Handler
Rack::Session を別gemへ 3.0 gem rack-session を追加
ミドルウェアからの body.each 禁止 3.0 ボディを包んで返す
複数値ヘッダは配列で渡す 3.0 改行区切りの文字列をやめる
rack.input が任意に 3.1 nil を想定して読む
Rack::File 削除 3.1 Rack::Files に置き換え
:unprocessable_entity が非推奨 3.1 :unprocessable_content を使う
Rack::Logger 削除 3.2 代替なし(ロガーは自前で)
env のキーは文字列必須 3.2 シンボルキーを入れない

:unprocessable_entity は rack 3.2.7 でもまだ422に変換されますが、「将来の版で削除される。:unprocessable_content を使うこと」という警告が出ます。Rails のコントローラで render status: :unprocessable_entity と書いている箇所が該当し、Rails 8.1 の scaffold はすでに新しい名前で生成します。

Rack各系列のサポート状況

rack の README に載っているサポート方針(2026年9月時点)は次のとおりです。

系列 サポート 最新版(リリース日)
3.2.x バグ修正とセキュリティ修正 3.2.7(2026-08-13)
3.1.x セキュリティ修正のみ 3.1.22(2026-08-13)
3.0.x サポート終了 3.0.18(2025-05-22)
2.2.x セキュリティ修正のみ 2.2.24(2026-08-13)
2.1.x以前 サポート終了 -

3.0 系は、3.1 系や 2.2 系より先に修正が止まっている点に注意が要ります。CHANGELOG によると、2026-04-01 には 3.2.6 で CVE 13件、3.1.21 で12件、2.2.23 で9件が修正されました(3.1 系で残った CVE-2026-26962 は 2026-08-13 の 3.1.22 で修正)。3.0 系には修正版が出ておらず、今後も提供されないため、保守中の系列への移行が必要です。rack 3.2 の gemspec が要求する Ruby は 2.4 以上なので、Rack本体の最低要件は満たしやすいものの、併用するサーバーやgemのRuby要件も確認が必要です。Ruby 自体の版とサポート期限は Rubyの最新バージョンで確認できます。

Rackと組み合わせるWebサーバーの選び方

Rack対応サーバーは共通のインターフェースでアプリを呼び出せますが、対応するRackの版やストリーミング機能、並行処理方式を確認して選びます。rack の README は Puma、Unicorn、Falcon、Pitchfork、Phusion Passenger、Thin、uWSGI、NGINX Unit などを対応サーバーとして挙げています。

  • Puma(現行 8.0.2):プロセスとスレッドを併用する。Rails の既定で、迷ったらこれを選ぶ
  • Unicorn:プロセスのみで動く。スレッドセーフでないコードを抱えたアプリ向けで、遅いクライアントを捌くには前段に nginx などのリバースプロキシが必須
  • Falcon:Fiber ベースの非同期サーバー。Rack 3 のストリーミングボディや HTTP/2 を活かす構成で候補になる
  • Pitchfork:Shopify が Unicorn から派生させたプロセス型サーバー

WEBrick は Ruby 3.0 で標準ライブラリから外れ、rackup の README も WEBrick は「可能なら避けるべき」としています。ローカルの動作確認も puma config.ru で足ります。

Rackを直接書くべき場面とフレームワークに任せる場面

Rack を直接書く価値があるのは、ルーティングが数本で済むヘルスチェックや Webhook の受け口、既存アプリの前に挟む認証・計測・ヘッダ付与、そしてフレームワークに依存させたくない gem の作成です。小さなRackアプリならアプリ側の依存をrack gemだけに絞れる場合がありますが、HTTPで公開するにはPumaなどのサーバーも別途必要です。ミドルウェアとして書いておけば Rails にも Sinatra にも同じコードを差し込めます。

逆に、画面やフォームを持つアプリを Rack だけで組むのは勧めません。CSRF 対策、セッション、パラメータの型変換、エスケープといった、フレームワークが既定で済ませている安全装置を自分で積むことになるからです。ルーティングが増えてきたら、Rack の上に薄く乗る Sinatra(現行 4.2.1、rack 3.0 以上 4 未満を要求)や Roda へ移り、画面が主役なら Rails を選ぶ、という線引きが現実的です。

よくある質問

rackupコマンドが「command not found」になるのはなぜですか?

Rack 3.0 から rackup コマンドは rack gem に含まれず、別の rackup gem になったためです。gem install rackup(Bundler なら bundle add rackup)で入ります。ただし本番では puma config.ru のようにサーバーのコマンドで直接起動するのが推奨です。gem の追加方法は gemとBundlerの使い方を参照してください。

Rack::Deflaterはdeflate圧縮にも対応していますか?

対応していません。扱えるのは gzip と identity(無変換)だけで、Brotli や zstd を使いたい場合は nginx や CDN 側で圧縮します。

使っているRackのバージョンはどう確認しますか?

アプリのディレクトリで bundle exec ruby -e 'require "rack"; puts Rack.release' を実行するか、Gemfile.lock の rack の行を見ます。rack 3.2 からは Rack::VERSION 定数でも取得できます。

PythonでRackに相当するものは何ですか?

同期処理の WSGI と、非同期処理の ASGI です。Rack の README は謝辞で WSGI を土台にしたと述べています。Django や Flask が WSGI、FastAPI が ASGI のアプリケーションとして動きます。

Rails 8.1はRack 2.2でも動きますか?

動きます。Rails 8.1.4 の actionpack が要求するのは rack >= 2.2.4 です。ただし 2.2 系はセキュリティ修正のみの保守段階なので、Rack 3.2 系へ上げておくのが安全です。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.07 テックブログ ファインズの不正アクセスと153万件の予約者情報|予約システムを使う店舗の報告と通知の手順
  2. 2026.10.19 テックブログ コミックシーモア不正アクセスで会員情報流出:再開時期・パスワード再設定と運営側のログ点検
  3. 2026.10.16 テックブログ バーガーキング情報漏洩どうしたらいい?約1,118万件の対象確認・補償・退会とメールの真偽
  4. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更・退会の手順
  5. 2026.10.14 テックブログ ブックオフ不正アクセス:約638万人の対象確認と本物メールの見分け方

RELATED POSTS 関連記事

目次