Rack 2-> Rack 3アップグレードガイド(翻訳)
本ドキュメントは現在作成中ですが、Rack 3の主な変更点のうち、サーバーやミドルウェアやアプリケーションをアップグレードするうえで知っておくべき概要について説明しています。
🔗 インターフェイスの変更
🔗 Rack 2とRack 3の互換性
ほとんどのアプリケーションは、特に以下のRack仕様の厳密な移行ポイントに従うことでRack 2およびRack 3と互換性を保てます。
- レスポンスの配列はfrozenにできなくなった
- レスポンスの
status
は100以上の整数が必須になった - レスポンスの
headers
はfrozenでないハッシュが必須になった - レスポンスヘッダーのキーには大文字を含められなくなった
rack.input
はリライト可能にするうえで必須ではなくなった- 以下のRack環境変数が必須設定されなくなった
rack.multithread
rack.multiprocess
rack.run_once
rack.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.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
を自分で呼び出すことで可能でした。
概要
MITライセンスに基づいて翻訳・公開いたします。