Rack 2-> Rack 3アップグレードガイド(翻訳)
本ドキュメントは現在作成中ですが、Rack 3の主な変更点のうち、サーバーやミドルウェアやアプリケーションをアップグレードするうえで知っておくべき概要について説明しています。
🔗 インターフェイスの変更
🔗 Rack 2とRack 3の互換性
ほとんどのアプリケーションは、特に以下のRack仕様の厳密な移行ポイントに従うことでRack 2およびRack 3と互換性を保てます。
- レスポンスの配列はfrozenにできなくなった
- レスポンスの
statusは100以上の整数が必須になった - レスポンスの
headersはfrozenでないハッシュが必須になった - レスポンスヘッダーのキーには大文字を含められなくなった
rack.inputはリライト可能にするうえで必須ではなくなった- 以下のRack環境変数が必須設定されなくなった
rack.multithreadrack.multiprocessrack.run_oncerack.version
rack.hijack?(部分ハイジャック)およびrack.hijack(完全ハイジャック)は独立したオプションになったrack.hijack_ioは完全に削除されたSERVER_PROTOCOLは必須キーとなり、リクエストで用いられるHTTPプロトコルとマッチするようになった- ミドルウェアはbodyで
#eachを呼べなくなった- ただし
#to_aryに応答する場合はbodyで#to_aryを呼べる
- ただし
Rack 3には、後方互換性のない機能変更が1件あります。
- レスポンスヘッダの値は、複数の値を扱うため
Arrayになる可能性がある- これに伴い、
\nを含むエンコード済みヘッダーはサポート対象外になる
- これに伴い、
Rack::Response#add_headerを用いることで互換性を保てます。これは、背後のフォーマットを気にせずにヘッダーを追加できます。
Rack 3には、直接的な後方互換性のない機能変更が1件あります。
- レスポンスのbodyが
#each(enumerableなbody)ではなく#call(ストリーミングのbody)に応答するようになった- これにより旧バージョンのレスポンスハイジャックと同等のことが行える
サーバーで部分的Rackハイジャックがサポートされている場合は、部分的Rackハイジャックを利用できます(あるいはこの振る舞いをミドルウェアにラップできます)。
🔗 config.ruのRack::Builder#runがブロックを受け取れるようになった
従来の Rack::Builder#runメソッドが受け取れたのは、呼び出し可能な(callable)引数のみでした。
run lambda{|env| [200, {}, ["Hello World"]]}
上は以下のようにシンプルに書き直せます。
run do |env|
[200, {}, ["Hello World"]]
end
🔗 レスポンスbodyを双方向ストリーミングで利用可能になった
従来は、rack.hijackレスポンスヘッダを双方向ストリーミング(WebSocketsなど)の実装に利用可能でした。
def call(env)
stream_callback = proc do |stream|
stream.read(...)
stream.write(...)
ensure
stream.close(...)
end
return [200, {'rack.hijack' => stream_callback}, []]
end
この機能はオプションですが、正しく使うのが難しい機能でした。変更後は、以下のようにstream_callbackをレスポンスbodyとして渡すことで同じことができるようになりました。
def call(env)
stream_callback = proc do |stream|
stream.read(...)
stream.write(...)
ensure
stream.close(...)
end
return [200, {}, stream_callback]
end
🔗 Rack::Sessionが別gemに切り出された
従来のRack::Sessionはrack gemに含まれていましたが、すべてのアプリケーションがこれを必要としているわけではなく、rackのセキュリティサーフェス(露出面)が増加してしまうため、rack-sessionという別gemに切り出して個別に更新することが決定されました。
rack-sessionを利用するアプリケーションは、今後このgemを個別に追加する必要があります。
gem 'rack-session'
これで、従来のセッション機能がすべて利用可能になります。
🔗 bin/rackup、Rack::Server、Rack::Handler、Rack::Lobsterが別gemに切り出された
従来のRackにはrackup実行ファイルが含まれていましたが、Rubyの"default gem"にWEBrickが含まれなくなったので、私たちはrackをwebrickに依存させるべきか別gemに切り出すかを選択しなければなり、後者を選択しました。これにより、rackupを「Rackインターフェイス」と独立させて迅速に設計および実装できるようになると期待しています。
Rack 3では以下をGemfileに記述する必要があります。
gem 'rackup'
これにより、従来のrackup機能がすべて利用可能になります。
Rack::Server、Rack::Handler、Rack::Lobsterもrackup gemに移動し、それぞれRackup::Server、Rackup::Handler、Rackup::Lobsterにリネームされました。
アプリをRack 3のRackup::Serverで起動するには以下のようにします。
require 'rackup'
Rackup::Server.start app: app, Port: 3000
🔗 config.ruのオートロードはrequire 'rack'しない限り無効になる
従来はrackupで'rack'がrequireされていたので、rack/directoryなどのRackモジュールがオートロードされていました。Rack 3では、以下のようにrequire 'rack'するか、特定のモジュールを明示的にrequireする必要があります。
+require 'rack'
run Rack::Directory.new '.'
または
+require 'rack/directory'
run Rack::Directory.new '.'
🔗 リクエストの変更点
🔗 rack.versionが必須でなくなった
従来はrack.versionでRackプロトコルバージョンを取得できましたが、実用性が乏しいため、要件から削除されました。
🔗 rack.multithread、rack.multiprocess、rack.run_onceが必須でなくなった
従来のサーバーは、Rackアプリケーション実行環境が参照できるようにこれらのキー(を通した設定値)を提供しようとしていましたが、遅すぎて実用的でないため、要件から削除されました。
🔗 rack.hijack?が部分ハイジャックのみに適用されるようになった
従来は、rack.hijack?が完全ハイジャックと部分ハイジャックの両方を制御していましたが、部分ハイジャックのみに適用されるようになりました(これでストリーミングbodyで置き換え可能になります)。
🔗 rack.hijackだけで完全ハイジャックを実行可能になった
従来はリクエストのenv内にrack.hijackが存在し、かつ rack.hijack?もtrueでなければなりませんでした。変更後は、rack.hijackコールバックが存在するだけで済むようになりました。
🔗 rack.hijack_ioが削除された
従来は完全ハイジャックを行うためにrack.hijackが呼び出されると、リクエストのenvへのrack.hijack_ioの設定を試みていました。しかしこれはミドルウェアがenv.dupを呼び出していると実行不可能になることが多かったため、この要件は完全に削除されました。
🔗 rack.inputはリライト可能にするうえで必須でなくなった
従来はリライト可能にする(io.seek(0))ためにrack.inputが必須でしたが、これは一般にファイルベースの場合にしか利用できないため、リクエストbodyを効率的にストリーミングするうえで妨げとなっていました。変更後のrack.inputはリライト可能にするうえで必須ではなくなりました。
🔗 無効なネステッドクエリが解析エラーになるようになった
従来のRack 2では、クエリ文字列a[b[c]]=xをa[b][c]=xと同じように解析可能でした。この無効な構文は公式にはサポートされていませんでしたが、一部のライブラリやアプリケーションではいずれにしろ使われていました。実装の詳細により、Rack 2は最終的に、この無効な構文を正しい構文の場合と同じように解析するようになっています。
Rack 3では実装が変更され、この無効な構文は正しい構文と同じように解析されなくなりました。
Rack::Utils.parse_nested_query("a[b[c]]=x")
# Rack 3 => {"a"=>{"b[c"=>{"]"=>"x"}}} ❌
# Rack 2 => {"a"=>{"b"=>{"c"=>"x"}}} ✅
ネステッドパラメータの正しい構文はa[b][c]=xであり、Rack 3互換とするためには、アプリケーションコード側で正しい構文に変更する必要があります。
Rack::Utils.parse_nested_query("a[b][c]=x")
# Rack 3 => {"a"=>{"b"=>{"c"=>"x"}}} ✅
# Rack 2 => {"a"=>{"b"=>{"c"=>"x"}}} ✅
詳しくは#2128を参照してください。
🔗 rack.inputはフォームやマルチパートのデータを消費した後にrewindしなくなった
従来は、フォームやマルチパートのデータを消費した後に.rewindが呼び出されていた。
この従来の振る舞いに合わせるには、Rack::RewindableInput::Middlewareを用いてbodyをrewind可能にしたうえで、.rewindを明示的に呼び出すこと。
🔗 レスポンスの変更点
🔗 レスポンスはミュータブルが必須になった
Rack 3ではレスポンスの配列[status, headers, body]がミュータブルであることが要求されます。レスポンスをfrozenにする既存のコードは変更が必要です。
NOT_FOUND = [404, {}, ["Not Found"]].freeze
def call(env)
...
return NOT_FOUND
end
上は以下のように書き換えること。
def not_found
[404, {}, ["Not Found"]]
end
def call(env)
...
return not_found
end
前のバージョンには微妙なバグがある点に注意してください。ヘッダーのハッシュは変更可能ですが、これらの変更が以後のリクエストに漏洩する可能性があります。
🔗 レスポンスのヘッダーはミュータブルなハッシュが必須になった
Rack 3のレスポンスヘッダーはミュータブルなハッシュが必須です(従来は#eachに応答しkey/valueペアをyieldする任意のオブジェクトが利用できました)。
従来は以下のように書けました。
def call(env)
return [200, [['content-type', 'text/plain']], ["Hello World"]]
end
今後は以下のようにハッシュのインスタンスを使わなければなりません。
def call(env)
return [200, {'content-type' => 'text/plain'}, ["Hello World"]]
end
これにより、必要に応じてミドルウェアがヘッダーを予測に基づいて更新できるようになります。
🔗 レスポンスヘッダーは小文字のみになった
Rack 3ではすべてのレスポンスヘッダーを小文字にする必要があります。これはレスポンスヘッダーのフェッチや更新をシンプルにするためです。従来はRack::HeadersHashのような形にする必要がありました。
def call(env)
response = @app.call(env)
# HeaderHash must allocate internal objects and compute lower case keys:
headers = Rack::Utils::HeaderHash[response[1]]
cache_response(headers['ETag'], response)
...
end
しかし変更後はHTTPヘッダーでは以下のように通常の形式を使う必要があります。
def call(env)
response = @app.call(env)
# A plain hash with lower case keys:
headers = response[1]
cache_response(headers['etag'], response)
...
end
既存のコードに使われている個別のヘッダーキーを手動で変更せずにRack 3で動くようにしたい場合は、Rack::HeadersをRack 3で利用できます。
headers = defined?(Rack::Headers) ? Rack::Headers.new : {}
Rack::HeadersはHashのサブクラスで、キーを自動で小文字にします。
headers = Rack::Headers.new
headers['Foo'] = 'bar'
headers['FOO'] # => 'bar'
headers.keys # => ['foo']
🔗 複数のレスポンスヘッダーはArrayでエンコードされる
レスポンスヘッダーの値にArrayを用いて複数の値を扱えます(\nエンコードヘッダーのサポートは終了しました)。Rack::Responseを使う場合は何もする必要はありません。しかしレスポンスヘッダーに手動で値を追加する場合は、値を以下のようにArrayにする必要があります。
def set_cookie_header!(headers, key, value)
if header = headers[SET_COOKIE]
if header.is_a?(Array)
header << set_cookie_header(key, value)
else
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
end
else
headers[SET_COOKIE] = set_cookie_header(key, value)
end
end
🔗 レスポンスbodyが#eachに応答するとは限らない
Rack 3では、レスポンスbodyの要件が厳しくなっています。従来のレスポンスbodyが応答する必要があったのは#eachのみで、#closeへの応答はオプションでした。さらに、#eachを呼び出してレスポンスをバッファリングしても安全かどうかを知る方法がありませんでした。
🔗 レスポンスbodyで#to_aryが公開されているとバッファリングされる
bodyが#to_aryに応答する場合、#eachを呼び出して生成されるものと同一の内容のArrayを返さなければなりません。bodyが#to_aryと#close の両方に応答する場合、その#to_aryの実装は#closeも呼び出さなければなりません。
従来は、レスポンスbodyがすぐ利用可能か(バッファリング可能か)、またはストリーミングしているチャンクかどうかを判定できませんでした。変更後は、この点が#to_aryによって明らかにされるようになりました。
def call(env)
status, headers, body = @app.call(env)
# Check if we can buffer the body into an Array, so we can compute a digest:
if body.respond_to?(:to_ary)
body = body.to_ary
digest = digest_body(body)
headers[ETAG_STRING] = %(W/"#{digest}") if digest
end
return [status, headers, body]
end
🔗 ミドルウェアはレスポンスbodyを直接変更してはならない
レスポンスbodyが#eachに応答しない可能性がある点にご注意ください。今後、bodyがenumerableかストリーミングかを判定するときは、#eachに応答するかどうかで判定しなければなりません。
bodyで#eachを直接呼び出してはいけません。代わりに、元のbodyで#eachを呼び出す新しいbodyを返すこと。
🔗 ステータスコードはIntegerでなければならない
レスポンスのステータスコードは、100以上のIntegerでなければなりません。
従来は#to_iに応答する任意のオブジェクトが許容されていたので、["200", {}, ""]のようなレスポンスを[200, {}, ""]に置き換える必要があります。これはステータスオブジェクト上で#to_iを自分で呼び出すことで可能でした。
関連記事
https://techracho.bpsinc.jp/hachi8833/2024_02_18/1198566
概要
MITライセンスに基づいて翻訳・公開いたします。