コンテンツにスキップ

ngx_mruby v2.7.1

ngx_mruby 2.7.1は、2.x系列のメンテナンスリリースで、セキュリティリリースでもあります。セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてある問題を直しました。また、テストスイートを拡充するときやコードをレビューするときに見つけた、複数の不具合も直しました。

修正の一部で、既存の設定やRubyのスクリプトから見える挙動が変わります。たとえば、ステータスコード、レスポンスのボディやその長さ、例外、nginxが次のフェーズへ進むかどうか、ハンドシェイクが受け取る証明書、nginxが起動するかどうかが変わります。前の挙動に合わせて書いたスクリプトは、変更が必要になることがあります。アップグレードの前に最初の節を読んでください。各項目では、2.7.0で見えた挙動を「変更前」に、2.7.1で見える挙動を「変更後」に書きました。挙動を変える必要があった理由は「変更の理由」に書き、「不具合」か「背景」、「前の挙動を残せなかった理由」、「ドキュメントが示していた挙動」の3行に分けました。続けて、影響を受ける設定と必要な対応を書きました。テストスイートが変更を試している項目には、そのテストのポートも書きました。「このリリースで変わらない挙動」には、関係する挙動のうち変わらないものを書きました。「既知の制限」の節には、このリリースで変えなかった挙動を挙げました。

アップグレードの前に読む挙動の変更

Section titled “アップグレードの前に読む挙動の変更”
ビルド、設定、スクリプトで使っているもの 読む項目
Redisとデフォルトのbuild_config.rb 1
mruby_output_body_filter*、mruby_output_header_filter*、またはNginx::Filter 2
Nginx.rputsかNginx.echoの後のraise 3
logハンドラか出力フィルタの中の、Nginx::AsyncかFiber.yield 4
再開した後に例外を投げるかNginx.redirectを呼ぶハンドラの中のNginx::Async.sleepかNginx::Async::HTTP.sub_request、またはsleepするlocationへのsub_request 5
mruby_enable_read_request_body on 6
Nginx::SSLかmruby_ssl_handshake_handler* 7
mruby_init*、mruby_init_worker*、mruby_exit_worker*、mruby_server_context_handler_code、ハンドシェイクのハンドラで呼ぶリクエストのメソッドかNginx.log、またはNginx::Server#add_listenerかNginx::Stream.add_listener、またはstreamのinit、init worker、exit worker、server contextのコード 8
mruby_set*と、同じリクエストで後に動くハンドラが呼ぶNginx::Async.sleep 9
http {}ブロックの無い設定 10
1つのlocationにmruby_output_header_filter*とmruby_output_body_filter*があり、ボディフィルタがNginx.rputsかNginx.echoを呼ぶ設定 11
POSTやPUTのようにボディのあるリクエストに、ボディを読まずにNginx.rputsかNginx.echoで応答するハンドラ 12
出力なしで終わることのあるmruby_content_handler*、またはresponse body is emptyに一致させるログの監視 13
post-readハンドラかaccessハンドラと組み合わせたmruby_enable_read_request_body onで、同じリクエストにmruby_log_handler*かmrubyの出力フィルタがあるもの 14

1. Redisはデフォルトのビルドに含まれなくなりました

Section titled “1. Redisはデフォルトのビルドに含まれなくなりました”
  • 変更前:build_config.rbにmatsumotory/mruby-redisが入っていたので、デフォルトの設定でビルドしたバイナリにはRedisクラスがありました。
  • 変更後:mruby-redisの行はコメントアウトしました(#528)。コメントアウトしたのは、このgemが依存するhiredisを、バージョンを固定していないブランチからcloneするためです。このcloneが原因で、ビルドが壊れました。デフォルトのbuild_config.rbでビルドしたバイナリにはRedisクラスが無く、Redisを参照するコードではNameErrorが発生します。そのメッセージは「uninitialized constant Redis」です。
  • 変更の理由:
    • 背景:macOS以外では、mruby-redisのビルドがhiredisをデフォルトブランチの先頭からcloneします。そのため、hiredisのデフォルトブランチが変わると、デフォルトのbuild_config.rbで作るすべてのビルドが壊れる可能性がありました。#528でbuild_config.rbに足したコメントには、-Werrorを付けたCIでこの種の失敗が起きたことが書いてあります。
    • 前の挙動を残せなかった理由:gemをデフォルトの一覧に残していたら、すべてのデフォルトのビルドがこの種の失敗にさらされたままでした。デフォルトのビルドは成功しなければなりません。一方、mruby-redisは自身のビルドスクリプトでhiredisをcloneするので、build_config.rbではhiredisのバージョンを固定できません。したがって、mruby-redisがhiredisを固定するまで、このgemはデフォルトの一覧に残せません。
    • ドキュメントが示していた挙動:2.7.0のドキュメントでは、mruby-redisが「the default mrbgems」の1つに入っていました(2.7.0のdocs/install)。また、Redisは「with default build configuration」で使えるクラスの1つに入っていました(2.7.0のbuilt-in mrbgems)。つまり、ドキュメントには前の挙動が書いてありました。docs/installには、追加のgemを入れてngx_mrubyをビルドする方法として、build_config.rbでmrbgemsを選ぶ方法が書いてあります。Redisを残すにも、この方法を使います。
  • 影響の範囲:Redisを使うスクリプトです。たとえば、READMEとdocs/use_caseの例がこれに当たります。同梱のauto-ssl mrbgemの利用者も影響を受けます。このmrbgemのクライアントは、Redisの接続を受け取るからです。独自のbuild_config.rbでビルドする場合は影響を受けません。
  • 必要な対応:Redisを使う場合は、ビルドの前に、build_config.rbのconf.gem github: 'matsumotory/mruby-redis'のコメントアウトを外してください。#528に書いたhiredisの問題が残っている間は、このgemのビルドが失敗することがあります。

2. 出力フィルタでのレスポンスの長さとNginx::Filter

Section titled “2. 出力フィルタでのレスポンスの長さとNginx::Filter”

この項目は4つの部分に分かれます。

レスポンスの長さは、ボディフィルタの出力に従います。

  • 変更前:mruby_content_handler*のようなmrubyのハンドラがNginx.rputsかNginx.echoでレスポンスのボディを書き、mruby_output_body_filter*がそれをNginx::Filter#body=か#outputで置き換えると、レスポンスの長さは、Nginx.rputsが出力した長さのままでした。置き換えたボディが長いとその長さで切れ、短いとレスポンスの一部でないバイトでその長さまで埋まりました。ボディフィルタ自身がNginx.rputsかNginx.echoを呼ぶと、Content-Lengthは、フィルタを通る前のレスポンスの長さに、その出力の長さを足した値になり、その出力自体は送られませんでした。ドキュメントには、ボディフィルタでのこの呼び出しを認める記述が、どのバージョンにもありません。これらのレスポンスは壊れていたので、これらに依存してよいものはありません。このケースを試すリポジトリ自身のテストは切れたレスポンスを期待しており、その期待値はこのリリースで変わります。この部分では、mrubyのヘッダフィルタの無いlocationについて書きます。ヘッダフィルタのあるlocationについては、4つ目の部分を見てください。
  • 変更後:ボディはボディフィルタの出力と同じになり、Content-Lengthはその長さになります。ボディフィルタがNginx.rputsかNginx.echoで書いたものは、送られず、Content-Lengthにも入りません。
location /filter/output_rputs_origin {
mruby_content_handler_code 'Nginx.rputs "origin"';
mruby_output_body_filter_code '
f = Nginx::Filter.new
f.output "[#{f.body}] and more"
';
}
バージョン Content-Length ボディ
2.7.0 6 [origi
2.7.1 17 [origin] and more
  • 変更の理由:
    • 不具合:ngx_mrubyは、ボディ用に保持している長さの分だけ、ボディフィルタのバッファからバイトを送り、Content-Lengthにも同じ数を設定します。2.7.0では、ngx_mrubyは、その長さにNginx.rputsが数えたContent-Lengthを使い、バッファにはボディフィルタの出力を使っていました。そのため、フィルタが長さを変えるたびに、長さとバッファが食い違いました。長いボディは切れ、短いボディの後では、ngx_mrubyが新しいボディの終わりより先のバイト、つまりレスポンスの一部でないメモリを送りました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:長さとバッファは一致しなければならず、バッファはフィルタの出力なので、長さは出力に従う必要があります。前の長さを残すと、ボディが切れるか、バッファの先まで読む挙動が残ります。
    • ドキュメントが示していた挙動:ドキュメントには、Nginx::Filter#body=について「Sets the string to the response body」と書いてあり、2.7.1のレスポンスはこの記述のとおりです。ほかのものが作ったレスポンスでは、2.7.0でも既にこの記述のとおりでした。2.7.0のドキュメントの同じ節(Nginx::Filter Class)には、「You can NOT use Nginx.rputs and Nginx.echo in the output body handlers」と書いてあります。
  • 影響の範囲:Nginx.rputsかNginx.echoで書いたボディの長さをボディフィルタが変えるlocationと、レスポンスを何が作ったかに関わらず、自分でNginx.rputsかNginx.echoを呼ぶボディフィルタです。長さを変えず、これらを呼ばないボディフィルタでは、何も変わりません。ファイル、return、proxy_pass、ほかのcontentハンドラが作ったレスポンスに掛かるボディフィルタも変わりません。これらでは、長さが既にフィルタの出力に従っていたからです。
  • 必要な対応:クライアントやテストが切れたレスポンスを期待していた場合を除き、ありません。ボディフィルタからはNginx.rputsとNginx.echoを取り除いてください。ボディフィルタでこれらが書いたものは送られません。
  • テスト:test/t/cases/filter_connection.rbのポート18112のテストです。

ボディを読む前に呼んだNginx::Filter#bodyが返す値が変わります。

  • 変更前:mruby_output_header_filter*ではボディをまだ読んでいません。そこで呼んだNginx::Filter#bodyは、Content-Lengthと同じ長さで、中身がレスポンスのボディでない文字列を返しました。長さが不明なとき、または長さが10485760バイト、すなわち10 MiB以上のときは、代わりにArgumentErrorが発生しました。
  • 変更後:そこでは空の文字列を返します。contentハンドラでは、既に空の文字列を返していました。
  • 変更の理由:
    • 不具合:ヘッダフィルタではボディをまだ読んでいません。2.7.0では、「変更前」に書いたとおり、Content-Lengthと同じ長さで中身がレスポンスのボディでない文字列が返るか、ArgumentErrorが発生しました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:その時点では返すボディが無いので、正しい結果は空の文字列だけです。長さは$sent_http_content_lengthで得られます。
    • ドキュメントが示していた挙動:ドキュメントには一貫して、Nginx::Filterは出力のボディフィルタで使うクラスだと書いてあります。2.7.0のドキュメントの記述は「designed to be used in the output body handlers」です(2.7.0のNginx::Filter Class)。
  • 影響の範囲:f.bodyを読むヘッダフィルタです。たとえば、レスポンスの長さを知るためにf.bodyのサイズを読むものが当たります。
  • 必要な対応:長さはNginx::Var.new.sent_http_content_lengthで読んでください。値はStringで、長さが不明なときはnilです。ボディはmruby_output_body_filter*で読んでください。
  • テスト:test/t/ngx_mruby.rbのポート18080のテストです。

Nginx::Filter#body=と#outputは、ボディフィルタの中でだけ使えます。

  • 変更前:これらはどこでも呼べました。ボディフィルタもあるlocationで、ボディフィルタ以外のハンドラがこれらを呼ぶと、そのボディフィルタが空でないレスポンスを読んだ時点でワーカーがクラッシュしました。locationにボディフィルタが無いときは、この呼び出しはレスポンスに何の影響もありませんでした。
  • 変更後:mruby_output_body_filter*の外ではRuntimeErrorが発生します。メッセージはNginx::Filter#body= can only be used in an output body filterです。ほかの例外と同じく、この例外でハンドラは終わります。
  • 変更の理由:
    • 不具合:ボディフィルタの外でNginx::Filter#body=か#outputでボディを設定すると、「変更前」に書いたとおり、locationのボディフィルタが空でないレスポンスを読んだときにワーカーがクラッシュしました。ボディフィルタが無いときは、何の影響もありませんでした。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:ボディフィルタは、レスポンスをバッファに読み込み、後でそのバッファを送ります。ボディフィルタがこのバッファを確保するのは、ボディがまだ設定されていないときだけです。Nginx::Filter#body=は、このバッファを設定するときに、ボディフィルタがバッファへ書き込む位置を設定しませんでした。そのため、後で動いたボディフィルタが空でないレスポンスを読んだ時点で、ワーカーがクラッシュしました。2.xでは、ボディフィルタが読んで送るボディと、Nginx::Filter#body=が設定するボディに、1つのバッファを使います。また、ボディフィルタの外でボディを設定する使い方は、一度もサポートしていませんでした。そのため、2.7.1では2つ目のバッファを足さずに、この呼び出しを拒否します。
    • ドキュメントが示していた挙動:ドキュメントには一貫して、Nginx::Filterはほかのハンドラでは動かないと書いてあります。2.7.0のドキュメントの記述は「It doesn’t work in other handlers like mruby_content_handler_code」です(2.7.0のNginx::Filter Class)。
  • 影響の範囲:ヘッダフィルタやほかのハンドラのうち、body=かoutputを呼ぶものです。
  • 必要な対応:ボディはmruby_output_body_filter*で設定してください。
  • テスト:test/t/ngx_mruby.rbのポート18080のテストです。

mrubyのヘッダフィルタがあるときは、ヘッダがボディフィルタを待ちます。

  • 変更前:mruby_output_header_filter*とmruby_output_body_filter*のあるlocationでは、ヘッダフィルタのコードが終わるとすぐにヘッダが出ていき、そのContent-Lengthは、ボディフィルタを通る前のレスポンスの長さでした。そのため、ボディを長くするボディフィルタはContent-Lengthが示すより多いバイトを送り、短くするボディフィルタは少ないバイトを送りました。Nginx.rputsで書くハンドラがレスポンスを作る場合、このようなlocationはまったく応答しませんでした。この件は「不具合の修正」の節を見てください。
  • 変更後:ヘッダは、ボディフィルタが終わるまで待ちます。ボディフィルタだけのlocationでは、既にそうなっていました。ヘッダには、ボディフィルタの出力の長さと、ヘッダフィルタが加えた変更が入ります。ヘッダフィルタのコードは、これまでどおりボディフィルタのコードより先に動きます。長さが分からないレスポンスは、前と同じく、ボディフィルタを通らずに次へ渡ります。
  • 変更の理由:
    • 不具合:ヘッダを先に送ると、レスポンスのContent-Lengthがその時点で決まり、その後でボディフィルタが長さを変えていました。Content-Lengthでレスポンスを読むクライアントは、違う長さのボディを正しく読めません。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:ボディの長さを決めるのはボディフィルタなので、ヘッダに正しいContent-Lengthを入れられるのは、ボディフィルタが動いた後だけです。ボディフィルタだけのlocationでは、同じ理由で既にヘッダを待たせていました。
    • ドキュメントが示していた挙動:両方のフィルタがあるlocationでヘッダをいつ送るかは、ドキュメントに書いてありませんでした。Nginx::Filter#body=については、「Sets the string to the response body」と書いてあります(2.7.0のNginx::Filter Class)。
  • 影響の範囲:mrubyのヘッダフィルタとmrubyのボディフィルタの両方があり、ボディフィルタがボディの長さを変えるlocationです。
  • 必要な対応:ありません。
  • テスト:test/t/cases/header_and_body_filter.rbのポート18150のテストです。レスポンスをファイルが作る場合と、Nginx.rputsで書くハンドラが作る場合を試します。

3. Nginx.rputsの後で例外が発生すると、レスポンスは500になります

Section titled “3. Nginx.rputsの後で例外が発生すると、レスポンスは500になります”
  • 変更前:post-read、server rewrite、rewrite、access、contentのいずれかのハンドラが、Nginx.rputsかNginx.echoで出力を書いてから例外を投げると、それまでに書いた出力を付けて200で応答しました。これは、先にNginx::Asyncで待たなかった場合です。accessハンドラかpost-readハンドラでは、nginxがその後のフェーズへ進んだので、locationのcontentハンドラも動き、その出力が後に続きました。
  • 変更後:リクエストにはnginxの500エラーページが返り、出力は破棄されます。その後のフェーズは動きません。出力なしで例外を投げるハンドラは、既にこの結果になっていました。Nginx::Asyncの後で例外を投げるハンドラも、この結果になります(項目5)。
    • mruby_set*:2.7.0では、出力がレスポンスとして送られ、変数にはハンドラが設定していない値が入りました。いまは出力が破棄され、変数は空の文字列になります。これはnilを返すハンドラと同じです。locationの前のディレクティブが変数に値を与えていた場合も、変数は空の文字列になります。リクエストは先へ進み、レスポンスのContent-Lengthには破棄した出力が入りません。ここまでは、ハンドラがNginx::Async.sleepかNginx::Async::HTTP.sub_requestで待つ前に例外を投げた場合です。Nginx::Async.sleepの後では、リクエストは先へ進まず、出力の無いnginxの500エラーページが返ります。2.7.0では、出力がレスポンスとして200で送られていました。Nginx::Async::HTTP.sub_requestの後では、nginxが変数を待たずにcontentフェーズへ進むので、例外に続く500を送れず、クライアントには何のレスポンスも届きません(項目5)。
    • ヘッダフィルタ:ヘッダフィルタで例外が発生したときは、2.7.0でも既にレスポンスのステータスが500になり、ボディには元のレスポンスのボディが残りました。2.7.0では、Nginx.rputsかNginx.echoを呼ぶヘッダフィルタでワーカーがクラッシュしました。この件は「不具合の修正」の節を見てください。そのようなヘッダフィルタがその後で例外を投げると、いまはその出力が破棄され、Content-Lengthは、元のレスポンスが持っていたボディの長さになります。破棄するのは、ヘッダフィルタ自身の出力だけです。レスポンスを作るのがNginx.rputsかNginx.echoで書くmrubyのcontentハンドラなら、レスポンスにはそのハンドラの出力とそのContent-Lengthが残ります。ヘッダフィルタが出力なしで例外を投げた場合も同じで、この場合は2.7.0も同じレスポンスを送っていました。
    • logハンドラとボディフィルタ:例外の後は、出力を送らなくなりました。
  • 変更の理由:
    • 不具合:例外の後、ngx_mrubyはmrb_run failed: return 500 HTTP status code to clientをログに出してステータスを500にしました。ところが、その後で出力を送るコードパスがステータスをリセットし、途中までの出力を200で送りました。そのため、失敗したハンドラが、クライアントには成功として伝わりました。accessハンドラかpost-readハンドラでは、このコードパスが、nginxが「先へ進む」と読む値も返しました。そのため、例外で失敗したアクセスチェックでも、リクエストが通りました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:例外が発生したら、ハンドラはエラーのステータスで終わらなければなりません。そうしないと、アクセスの判定を誤ります。出力を送るコードパスでは、200を送りながらフェーズを止められないので、出力を破棄します。クライアントには、error.logの行が前から示していた500が届きます。これは、出力なしの例外の後やNginx::Asyncの後と同じです。mruby_set*のハンドラはリクエストに応答しないので、例外の後もリクエストは先へ進みます。そのため、破棄した出力は、その後に続くレスポンスの長さに入ってはいけません。また、変数には決まった値が要ります。2.7.0では、nginxのスクリプトエンジンがその時点で持っていた値が、そのまま変数に入りました。
    • ドキュメントが示していた挙動:例外を投げるハンドラについては、ドキュメントに何も書いてありませんでした。ドキュメントには、出力を書いてからNginx.return Nginx::HTTP_OKを呼び、returnで抜けるcontentハンドラの例があります(2.7.0のNginx.return)。このハンドラは、「必要な対応」に示すとおり、2.7.0でも2.7.1でも200で応答します。
  • 影響の範囲:出力を書いた後でハンドラを止めるためにraiseを使うスクリプト、メッセージを書いてから例外を投げるアクセスチェック、出力を書いてから例外を投げるmruby_set*のハンドラです。
  • 必要な対応:raiseは、エラーのためだけに使ってください。出力を返してcontentハンドラを途中で終えたいときは、例外を使わずに終えてください。
# 2.7.0: 200 "under maintenance" 2.7.1: 500
Nginx.rputs "under maintenance"
raise "stop"
# 2.7.0 and 2.7.1: 200 "under maintenance"
Nginx.rputs "under maintenance"
Nginx.return Nginx::HTTP_OK
return # leaves the handler; omit it when nothing follows

200以外のステータスでNginx.returnを呼ぶと、2.7.0と同じく、nginxはNginx.rputsの出力を付けずに、そのステータスのnginx自身のページを送ります。accessハンドラとpost-readハンドラでは、出力を書かないでください。書いた出力は、ハンドラが終わるときにレスポンスとして送られます。Nginx::HTTP_OKを返すかステータスを設定しない場合は、nginxがその後のフェーズへ進み、そのフェーズの出力がContent-Lengthを超えてレスポンスの後に続きます。これは2.7.0でも2.7.1でも同じで、この件は「既知の制限」の節に書きました。accessハンドラやpost-readハンドラでリクエストを止めるには、Nginx.return Nginx::HTTP_FORBIDDENのようにエラーのステータスを返してください。

  • このリリースで変わらない挙動:Nginx.rputsの後でNginx.return、Nginx.send_header、Nginx.status_code=のどれかを呼んでから例外を投げたハンドラは、既に500になっていました。例外を投げる前にローカルのURIへNginx.redirectしたハンドラは、これまでどおり、リダイレクト先のレスポンスで終わります。
  • テスト:test/t/cases/sync_exception.rbのポート18143のテストです。

4. logハンドラと出力フィルタでは、Nginx::Asyncで例外が発生します

Section titled “4. logハンドラと出力フィルタでは、Nginx::Asyncで例外が発生します”
  • 変更前:mruby_log_handler*でNginx::Async.sleepを呼ぶと、nginxが既に解放している途中のリクエストにタイマーが残り、タイマーが切れたときにワーカーがクラッシュすることがありました。logハンドラでNginx::Async::HTTP.sub_requestを呼ぶと、サブリクエストが始まりました。mruby_output_header_filter*とmruby_output_body_filter*では、Nginx::Async.sleepを呼んだ位置でフィルタが止まりました。レスポンスはフィルタを通らずに、元のレスポンスのステータスで出ていきました。テストした設定では、そのステータスは200でした。タイマーが切れる前にリクエストが終わるので、sleepの後のコードは動きませんでした。ヘッダフィルタでsub_requestを呼ぶと、サブリクエストが始まりました。これらのハンドラで直接Fiber.yieldを呼ぶと、メッセージなしでハンドラが止まりました。
  • 変更後:これらのハンドラでは、どちらのメソッドでもRuntimeErrorが発生します。メッセージは、たとえばNginx::Async.sleep is not available in a log handlerで、ヘッダフィルタでは... in a header filter、ボディフィルタでは... in a body filterです。例外は、タイマーをセットする前、サブリクエストを始める前に発生します。この例外は、ほかの例外と同じように扱われます。error.logに[error]の行が出て、フィルタではレスポンスのステータスが500になります。テストした設定では、そのボディはフィルタを通っていないボディでした。logハンドラでは、レスポンスは既に送ってあります。これらのハンドラで直接Fiber.yieldを呼ぶと、... yielded its fiber, but nginx cannot resume it thereがログに出て、フィルタではステータスが500になります。
  • 変更の理由:
    • 不具合:nginxは、リクエストを解放する途中でlogフェーズを動かします。それは、リクエストのクリーンアップの後で、リクエストのメモリを解放する直前です。そのため、logフェーズでセットしたタイマーは解放済みのメモリの上で発火し、もう存在しないリクエストでハンドラが再開しました。出力フィルタはngx_http_send_headerとngx_http_output_filterの中で動き、この2つはすぐに呼び出し元へ戻ります。そのため、出力フィルタの中でyieldしたハンドラはただ止まり、フィルタの処理が飛ばされます。後でタイマーかサブリクエストがハンドラを再開しても、レスポンスは既に出ていっています。直接のFiber.yieldは、一度も再開されません(#551)。
    • 前の挙動を残せなかった理由:これらの場所でハンドラを再開しても、nginxには、レスポンスをまだ変えられる時点がありません。そのため、この呼び出しは、タイマーをセットする前、サブリクエストを始める前に失敗しなければなりません。
    • ドキュメントが示していた挙動:ドキュメントには一貫して、Nginx::Asyncが動く場所として、set、rewrite、accessのハンドラが挙げてあります。logハンドラとフィルタは、その中に一度も入っていません(2.7.0のNginx::Async Class)。
  • 影響の範囲:Nginx::Async.sleep、Nginx::Async::HTTP.sub_request、Fiber.yieldを呼ぶlogハンドラと出力フィルタです。
  • 必要な対応:rewriteハンドラかaccessハンドラ、またはmruby_set_codeの中で待ち、結果はnginxの変数でフィルタやlogハンドラへ渡してください。
  • このリリースで変わらない挙動:mruby_set*と、post-read、server rewrite、rewrite、access、contentの各ハンドラは、これまでどおり両方のメソッドを使えます。contentハンドラについては、「既知の制限」の節を見てください。
  • テスト:test/t/cases/async_sleep_in_log.rbのポート18129のテストと、test/t/cases/async_sleep_in_output_filter.rbのポート18131のテストです。

5. Nginx::Asyncの後で例外が発生すると、レスポンスは500になります

Section titled “5. Nginx::Asyncの後で例外が発生すると、レスポンスは500になります”

この項目では、Nginx::Async.sleepかNginx::Async::HTTP.sub_requestが再開したハンドラについて書きます。対象は、そのハンドラが例外を投げる場合と、Nginx.redirectを呼んでから例外を投げるかNginx.returnを呼ぶ場合です。sleepするlocationへのsub_requestも対象です(#550)。

  • 変更前:
    • 出力が無いときは、nginxがレスポンスなしで接続を閉じました。
    • Nginx.rputsの後では、クライアントには、それまでに書いた出力を付けた200が返りました。
    • ステータスを設定した後、たとえばNginx.return 503の後では、クライアントにはそのステータスが返りました。serverレベルのハンドラのように、同じリクエストのハンドラが前にNginx.return Nginx::DECLINEDを呼んでいたときは、nginxが次のフェーズへ進みました。たとえば、ファイルを200で返しました。
    • 同じワーカーで次にいずれかのmrubyのハンドラを動かすリクエストは500で失敗し、error.logには前の例外がもう一度出ました。
  • 変更後:リクエストにはnginxの500エラーページが返り、出力は破棄されます。次のリクエストは影響を受けません。
    • レスポンスが既に始まっていたときは、500を送れません。nginxはheader already sentをログに出してリクエストを終え、クライアントには、それまでに送ったものが残ります。Nginx::Async.sleepの後でローカルのURIへNginx.redirectしたときは、クライアントに残るのはリダイレクト先のレスポンスです。#550のために測った2つのケースでは、2.7.0ではレスポンスが届いていたのに、いまはクライアントに何のレスポンスも届きません。1つ目は、Nginx::Async::HTTP.sub_requestの後のNginx.redirectです。2.7.0では、リダイレクト先のレスポンスが送られた後、クライアントが閉じるまで接続が開いたままでした。2つ目は、sub_requestの後で例外を投げるmruby_set_codeです。nginxは、contentフェーズの前に変数を待ちません。2.7.0では、contentハンドラの出力を付けた200が送られていました。
    • 再開したハンドラでNginx.redirectの後に例外かNginx.returnが続くと、nginxは、リダイレクト先のレスポンスのすぐ後で接続を閉じます。HTTP/1.1のkeep-aliveの接続では、その後ろにパイプラインで送ったリクエストにはレスポンスが返りません。クライアントは、新しい接続でそのリクエストを送り直す必要があります。2.7.0では接続が開いたままでしたが、パイプラインで送ったリクエストにそのリクエストのレスポンスが返らない点は同じでした。例外の後では、ワーカーが持ち続けた例外による500エラーページが返りました。これは、上の「変更前」の最後の点に書いた挙動です。Nginx.returnの後では、クライアントかタイムアウトが接続を閉じるまで、何も返りませんでした。
    • Nginx::Async.sleepで待つハンドラのあるlocationへのNginx::Async::HTTP.sub_requestは、いまは完了します。2.7.0では、クライアントかsend_timeoutがリクエストを終えるまで、何も送られませんでした。そのlocationが例外を投げたときは、Nginx::Async::HTTP.last_response.statusは0です。
  • 変更の理由:
    • 不具合:再開したハンドラで例外が発生した後、ngx_mrubyはreturn 500 HTTP status code to clientをログに出し、その後で500を、例外の前にハンドラが持っていたステータスに置き換えました。そのステータスは、0、つまりレスポンスなしか、途中までの出力を付けた200か、nginxが次のフェーズへ進むNginx::DECLINEDのような値でした。例外は、ワーカーのすべてのリクエストが共有するmrubyの状態にも残り、次のリクエストがその例外で失敗しました。2.7.0では、Nginx.redirectの後にステータスを付けてNginx.returnを呼ぶと、クライアントかタイムアウトが終えるまで、リクエストとその接続が開いたままでした(#550)。
    • 前の挙動を残せなかった理由:例外の後は、その状態をクリアしなければなりません。そうしないと、ワーカーの次のリクエストが失敗します。ステータスも、エラーのままでなければなりません。そうしないと、nginxは失敗したハンドラを成功として扱います。nginxは、タイマーの中で終えたリクエストを完了させるとき、そのリクエストを終える間にキューへ入れたposted requestsを実行します。2.7.1では、500か、Nginx.returnで設定したステータスがnginxに届くのは、リダイレクト先のレスポンスを送った後です。そのため、nginxは接続を閉じる以外の方法でリクエストを終えられず、posted requestsを実行すると、すぐに接続が閉じます。keep-aliveの接続がリダイレクト先のレスポンスの後で閉じるようになったのは、このためです。クライアントには、error.logの行が示す500が届きます。これは、待たなかったハンドラで例外が発生した後と同じです。
    • ドキュメントが示していた挙動:2.7.0のREADMEには、スクリプトが共有するmrubyの状態の「the exception flag」を、サーバーのプロセスが解放すると書いてあります。解放する目的は「extend safety by preventing interference between each script」です(Abstract)。前のリクエストの例外で次のリクエストが失敗するのは、この干渉に当たります。Nginx::Asyncが再開したハンドラでの例外と、そのようなハンドラでのNginx.redirectについては、ドキュメントに何も書いてありませんでした。ドキュメントには、Nginx::Async.sleepとNginx::Async::HTTP.sub_requestが動く場所として、set、rewrite、accessのハンドラが挙げてあります(2.7.0のNginx::Async Class)。
  • 影響の範囲:Nginx::Asyncを使い、その後で例外を投げることのあるハンドラ、レスポンスなしで接続が閉じる挙動や例外の前に設定したステータスに頼っていたクライアント、sub_requestを使うmruby_set_codeのハンドラです。再開したハンドラでNginx.redirectを呼んでから例外を投げるかNginx.returnを呼ぶlocationに、keep-aliveの接続でリクエストをパイプラインで送るクライアントも、影響を受けます。
  • 必要な対応:例外の後に500以外を返したいハンドラは、ハンドラの中で例外をrescueし、そこでステータスを設定してください。
  • テスト:test/t/cases/async_exception_state.rbのポート18130のテストと、test/t/cases/async_subrequest_raise.rbのポート18124のテストです。

6. mruby_enable_read_request_bodyで、どのハンドラがボディ全体を待つか

Section titled “6. mruby_enable_read_request_bodyで、どのハンドラがボディ全体を待つか”

mruby_enable_read_request_body onのとき、ngx_mrubyは、ハンドラのコードを動かす前にリクエストボディを読みます。この項目は2つの部分に分かれます。1つ目は、まだそろっていないボディを待てるハンドラ、2つ目は待てないハンドラです。

post-read、server rewrite、rewrite、access、contentの各ハンドラは待ちます。

  • 変更前:ハンドラが始まったときにリクエストボディが全部届いていないと、mruby_server_rewrite_handler*、mruby_rewrite_handler*、mruby_content_handler*は一度も動きませんでした。クライアントにはレスポンスが返らず、クライアントが接続を閉じても、client_body_timeoutが切れても、nginxはリクエストを終えませんでした。大きなアップロードや遅いクライアントのように、ボディが2回以上の読み込みに分かれて届くだけで、この状態になりました。post-readハンドラとaccessハンドラはボディを待ち、ボディを受け取って動きました。しかし、そのように待った後では、auth_requestのようにサブリクエストを待つ後のフェーズのハンドラが、再開しませんでした。この場合もクライアントにはレスポンスが返らず、クライアントが閉じた後も接続が開いたままでした。
  • 変更後:ハンドラは、nginxがボディを全部読んだ後に動きます。続いて、リクエストは後のフェーズへ進み、auth_requestのようにサブリクエストを待つフェーズのハンドラも動きます。keep-aliveの接続では、次のリクエストにも通常どおり応答し、ハンドラで例外が発生した場合は500を返します。クライアントが先に接続を閉じると、nginxはclient prematurely closed connectionをログに出してリクエストを終えます。client_body_timeoutが切れると、nginxはレスポンスなしで接続を閉じ、アクセスログには408が記録されます。どちらの場合も、ハンドラは動きません。そのようなリクエストのlogハンドラとフィルタについては、項目14を見てください。
  • 変更の理由:
    • 不具合:ボディがそろっていない間、ngx_mrubyはNGX_AGAINを返していました。nginxは、rewriteフェーズとcontentフェーズでこの値を「終わった」と読みます。そのため、nginxはボディの読み込みをやめて、client_body_timeoutを外しました。ハンドラは、二度と動きませんでした。クライアントには応答が無く、クライアントが去った後も、ワーカーが終了するまでリクエストとその接続が開いたままでした。post-readハンドラかaccessハンドラがボディを待った後は、ngx_mrubyがフェーズを動かし直しました。このとき、書き込みイベントのハンドラは、nginxがボディを読む間に設定する何もしないハンドラのままでした。nginxは、auth_requestのようにサブリクエストを待つフェーズのハンドラを、この書き込みイベントのハンドラから再開します。そのため、そのハンドラは再開しませんでした。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:これらのフェーズでリクエストを待たせておけるのは、ハンドラがNGX_DONEを返したときだけです。リクエストを終えつつボディを読み続ける戻り値はありません。そのため、ハンドラは待つ必要があり、ngx_mrubyは、ボディがそろったときにフェーズを動かし直します。サブリクエストを待つフェーズのハンドラは、書き込みイベントのハンドラからしか再開しません。そのため、ngx_mrubyは、nginxのmirrorモジュールと同じく、フェーズを動かし直す前に書き込みイベントのハンドラを元に戻すようになりました。
    • ドキュメントが示していた挙動:いまは、ハンドラがボディを受け取ります。ドキュメントには、このディレクティブの動作として「reads a HTTP request body before calling mruby code」と書いてあり、2.7.1の動作はこの記述のとおりです。2.7.0のこのディレクティブのドキュメント(mruby_enable_read_request_body)には、「a request body is not always read when invoking a handler like mruby_content_handler」と書いてあります。同じドキュメントには、「The directive force to read the request body」ともあります。
  • 影響の範囲:このディレクティブをserver rewrite、rewrite、contentのハンドラと使う設定です。このディレクティブをpost-readハンドラかaccessハンドラと使い、その後にauth_requestか、サブリクエストを待つほかのフェーズのハンドラが続く設定も、影響を受けます。
  • 必要な対応:ありません。
  • テスト:test/t/cases/request_body_later.rbの、ポート18144とポート18147のテストです。ポート18144のテストは、5種類のハンドラ、keep-alive、client_body_timeout、ボディの後の例外を試します。ポート18147のテストは、rewriteハンドラとaccessハンドラの後のauth_requestを試します。

mruby_set*、logハンドラ、出力フィルタは待ちません。

  • 変更前:これらのハンドラも、ほかのハンドラと同じく、コードを動かす前にボディを読み始めました。ボディが既に全部届いていたときのように、nginxがボディを一度で全部読めたときは、コードはボディを受け取って動きました。そうでないときは、コードはその時点では動かず、mruby_set*の変数には、ハンドラが設定していない値が入りました。ngx_mrubyが始めたボディの読み込みは残りました。その読み込みが完了すると、リクエストのフェーズが、そのときnginxが処理していたフェーズからもう一度動きました。nginx 1.31.6では、前のハンドラがボディを読んでいないHTTP/2のリクエストのすべてで、これが起きました。nginxがそのようなボディをイベントで読むからです。テストした設定では、mruby_set_codeと2つに分かれて届くボディで、次のことが起きました。
    • 同じlocationのmrubyのcontentハンドラが2回動き、ボディの最初の部分だけで応答しました。
    • proxy_passでは、upstreamのサーバーが、届いた部分だけを受け取りました。upstreamのサーバーが残りを待つときは、クライアントにレスポンスが返りませんでした。待たないときは、クライアントにレスポンスは返りましたが、nginxはHTTP/1.0の接続を閉じませんでした。
    • クライアントが閉じた後も、接続が開いたままでした。
  • 変更後:これらのハンドラがボディを読むのは、読み込みがすぐに完了するときだけです。それは、前のハンドラかモジュールがボディを読んだとき、リクエストにボディが無いとき、Content-LengthのあるHTTP/1.xのリクエストでボディ全体がヘッダのバッファに入っているときです。ヘッダのバッファは、nginxがリクエストヘッダを読み込んだバッファで、デフォルトで1kのclient_header_buffer_sizeのバッファか、ヘッダが必要としたlarge_client_header_buffersのバッファです。それ以外のときは、コードはボディなしですぐに動きます。Nginx::Request#bodyはnilを返し、error.logにはinfoレベルの行が1行出ます。その行の末尾はthe request body is not complete and an mruby_set handler cannot wait for it, so it runs without the bodyで、ハンドラによって、an mruby_set handlerの部分がa log handler、a header filter、a body filterになります。chunkedのボディ、HTTP/2かHTTP/3のボディ、そのバッファに一部だけが入っているボディは、残りが既に届いていても、そろっていないものとして扱います。待てる後のハンドラとproxy_passは、これまでどおりボディ全体を読みます。post-read、server rewrite、rewrite、access、contentのハンドラがまだ待っているボディも、そろっていないものとして扱います。nginxがその読み込みを打ち切ったときは、Nginx::Request#bodyは、ボディのうちnginxが読んだ部分を返します(項目14)。
  • 変更の理由:
    • 不具合:すぐに完了しなかった読み込みは、ハンドラが戻った後もリクエストへの参照を持ち続けました。その読み込みが完了すると、リクエストのフェーズが、そのときnginxが処理していたフェーズからもう一度動き、上に書いた結果になりました。
    • 前の挙動を残せなかった理由:これらのハンドラは待てません。mruby_set*は、nginxのrewriteモジュールが実行するスクリプトの中で動きます。logハンドラはnginxがリクエストを解放する途中で動き、フィルタはnginxがレスポンスを送る途中で動きます。nginxは、これらが戻るとすぐに先へ進みます。そのため、このようなハンドラは、既にそろっているボディを受け取って動くか、ボディなしで動くしかありません。ngx_mrubyは、読み込みを始める前に、Content-Lengthとnginxのヘッダのバッファにあるバイトから、これを決めます。chunkedのボディにはContent-Lengthが無く、HTTP/2かHTTP/3のボディはイベントで読むので、そろっていないものとして扱います。
    • ドキュメントが示していた挙動:2.7.0のこのディレクティブのドキュメントには、ngx_mrubyが「reads a HTTP request body before calling mruby code」と書いてありました(mruby_enable_read_request_body)。待てないハンドラは、挙げてありませんでした。2.7.0でも2.7.1でも、これらのハンドラはボディの残りを待ちません。2.7.0では、始めた読み込みがすぐに完了しないと、コードが動きませんでした。2.7.1では、コードがすぐに動きます。そのとき、ボディは無いか、待っているハンドラの読み込みをnginxが打ち切った後なら、nginxが読んだ部分になります(項目14)。いまのディレクティブのドキュメントには、これらのハンドラと、ボディなしで動く場合が書いてあります。
  • 影響の範囲:mruby_enable_read_request_body onの設定で、mruby_set*のハンドラ、logハンドラ、出力フィルタがリクエストボディを読むものです。2.7.1では、これらのハンドラは次のボディを受け取りません。それは、chunkedのボディ、HTTP/2かHTTP/3で届くボディ、ヘッダと一緒にnginxのヘッダのバッファに収まらないボディ、ヘッダの後に届くボディです。ただし、前のハンドラかモジュールがボディを読んでいた場合は受け取ります。2.7.0では、これらのボディのうちいくつかを、すぐに読めていました。たとえば、ヘッダと一緒に届いたchunkedのボディや、既に届いていたもっと大きなボディです。
  • 必要な対応:これらのハンドラにボディ全体を渡すには、これらより前に動く、待てるハンドラで、先にボディを読んでください。たとえば、mruby_enable_read_request_body onを付けたserverレベルのmruby_post_read_handler_codeです。そのハンドラが動いた後は、後に動くハンドラにとって、ボディはそろっています。
  • テスト:test/t/cases/request_body_later.rbの、ポート18148とポート18149のテストです。

7. Nginx::SSLはハンドシェイクのハンドラの中だけで動きます

Section titled “7. Nginx::SSLはハンドシェイクのハンドラの中だけで動きます”
  • 変更前:Nginx::SSLのメソッドをmruby_ssl_handshake_handler*の外で呼ぶと、ワーカーがクラッシュするか、意味の無い値が返ることがありました(#339)。対象のメソッドは、Nginx::SSL.new、#servername、証明書のセッター、#local_port、#tls_version、Nginx::SSL.logです。ドキュメントでは、この使い方はサポートの対象外でした。コードがNginx::SSL.newを呼ばないハンドシェイクは、同じワーカーの前のハンドシェイクが設定した証明書を受け取ることがありました。前のハンドシェイクが使った接続のメモリが再利用されていたときは、ハンドシェイクが失敗することもありました。
  • 変更後:ハンドシェイクのハンドラの外では、RuntimeErrorが発生します。メッセージはNginx::SSL can only be used in mruby_ssl_handshake_handlerです。コードが証明書を設定しないハンドシェイクは、サーバーのssl_certificateとssl_certificate_keyを使います。Nginx::SSL.newを呼ばないハンドシェイクも、これに含まれます。
  • 変更の理由:
    • 不具合:「変更前」に書いたとおり、ハンドシェイクのハンドラの外では、Nginx::SSLでワーカーがクラッシュするか、意味の無い値が返ることがありました(#339)。コードがNginx::SSL.newを呼ばないハンドシェイクは、前のハンドシェイクの証明書を受け取るか、失敗することがありました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:Nginx::SSLは、ハンドシェイクが終わった後も、ハンドシェイクの状態をサーバーの設定に残していました。この状態は、接続、サーバー名、証明書の文字列です。そのため、ハンドシェイクの外で呼んだメソッドは、まだハンドシェイクが動いていないときはNULLポインタを読みました。これが#339のクラッシュです。ハンドシェイクが動いた後に呼ぶと、メソッドはもう有効でないメモリを読みました。また、コードがNginx::SSL.newを呼ばない後のハンドシェイクは、前の接続の文字列を使い回しました。ハンドシェイクの外には接続もサーバー名も無いので、メソッドには返せる正しい値がありません。また、ハンドシェイクが終わるときに、この状態をクリアしなければなりません。
    • ドキュメントが示していた挙動:ドキュメントには2.1.4から、Nginx::SSLはハンドシェイクのハンドラで使うもので、ほかのハンドラでは動かないと書いてあります(2.7.0のNginx::SSL Class)。また、ssl_certificateは、証明書を選ばないハンドシェイクにnginxが使う証明書です。
  • 影響の範囲:ハンドシェイクのハンドラの外でNginx::SSLを呼ぶハンドラと、一部のハンドシェイクでNginx::SSL.newを呼ばないハンドシェイクのハンドラです。前者の例は、servernameを読むcontentハンドラです。後者の例は、条件によって早く戻るコードや、オブジェクトを一度だけ作って持ち続けるコードです。毎回のハンドシェイクでNginx::SSL.newを呼ぶハンドシェイクのハンドラは、証明書の変更の影響を受けません。
  • 必要な対応:Nginx::SSLは、ハンドシェイクのハンドラの中でだけ呼んでください。値を後で使うには、そこで値を保存してください。たとえば、ドキュメントの例のようにUserdataを使います。ハンドラが一部のハンドシェイクでだけ証明書を設定し、ほかのハンドシェイクでNginx::SSL.newを呼んでいない場合、そのほかのハンドシェイクは、いまはssl_certificateを受け取ります。
  • テスト:test/t/ngx_mruby.rbの、ポート18089とポート18090のテストです。

8. リクエストの外でリクエストのメソッドを呼ぶと、RuntimeErrorが発生します

Section titled “8. リクエストの外でリクエストのメソッドを呼ぶと、RuntimeErrorが発生します”

mruby_init*、mruby_init_worker*、mruby_exit_worker*、mruby_ssl_handshake_handler*、mruby_server_context_handler_codeのコードは、リクエストの外で動きます。mruby_init*とmruby_server_context_handler_codeはnginxが設定を読む間に動き、ほかはワーカーの中で動きます。streamモジュールでは、init、init worker、exit worker、server contextのコードが、セッションの外で動きます。この項目は2つの部分に分かれます。

リクエストの外では、リクエストのメソッド、セッションのメソッド、Nginx.logの挙動が変わります。

  • 変更前:リクエストスコープのメソッドは、ワーカーが最後に処理したリクエストを持ち続けるポインタから、いまのリクエストを探していました。
    • まだリクエストを処理していない場所では、これらのメソッドを呼ぶとクラッシュしました。mruby_init*とmruby_server_context_handler_codeでは、設定を読んでいる最中のnginx自身がクラッシュし、mruby_init_worker*ではワーカーがクラッシュしました。
    • mruby_exit_worker*とmruby_ssl_handshake_handler*では、ワーカーが最後に処理したリクエストを読みました。そのリクエストのメモリは既に解放されていることがあり、その場合は意味の無いデータを読むか、クラッシュしました。ワーカーがまだリクエストを処理していないとき、たとえば最初のハンドシェイクでは、mruby_init_worker*と同じくクラッシュしました。
    • Nginx.logとNginx.errloggerでは、まだリクエストを処理していない場所でRuntimeErrorが発生しました。そのため、mruby_init*とmruby_server_context_handler_codeでは、設定が無効になりました。ワーカーが既にリクエストを処理していた場合は、最後に処理したリクエストを通してログを書いていました。
    • streamモジュールの、init、init worker、exit workerのコードにも、同じ問題がありました。
  • 変更後:リクエストの外では、リクエストスコープのメソッドでRuntimeErrorが発生します。メッセージはthis method can only be used while processing a requestです。対象は、Nginx::Request、Nginx::Var、Nginx::Headers_in、Nginx::Headers_out、Nginx::Connection、Nginx::Server、Nginx::Filter、Nginx::Upstream、Nginx::Asyncのメソッドと、Nginx.rputs、Nginx.echo、Nginx.return、Nginx.send_header、Nginx.status_code=、Nginx.redirectです。Nginx::Serverのうち、add_listenerは対象から外れます。nginxが設定を読む間に動くコードでは、そこでのほかの例外と同じく、コードがこの例外をrescueしなければ、設定は無効になります。Nginx.logとNginx.errloggerは、リクエストの外ではnginxのエラーログに書きます。streamのinit、init worker、exit worker、server contextのコードでは、Nginx::Stream::Connection.new、そのメソッドのうちセッションを読むもの、Nginx::Stream::Async.sleepで例外が発生します。メッセージはthis method can only be used while processing a stream sessionです。Nginx::Stream::Connection.stream_statusとstream_status=では、例外は発生しません。Nginx::Stream.logは、nginxのエラーログに書きます。
  • 変更の理由:
    • 不具合:「変更前」に書いたとおり、リクエストの外では、リクエストスコープのメソッドでnginxかワーカーがクラッシュするか、ワーカーが最後に処理したリクエストが使われました。Nginx.logとNginx.errloggerでは、例外が発生するか、そのリクエストを通してログが書かれました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:リクエストのメソッドは、プロセス全体で共有するポインタからリクエストを探していました。このポインタは、mrubyのコードがリクエストのために動き始めたときに設定され、クリアされませんでした。リクエストの外のコードでは、このポインタはNULLか、ワーカーが最後に処理したリクエストを指していました。NULLのときは、プロセスがクラッシュしました。最後に処理したリクエストのメモリは既に解放されていることがあり、その場合、メソッドは無効なメモリを読みました。Nginx.logは、そのリクエストの接続を通してログを書きました。リクエストの外には返すべきリクエストが無いので、メソッドはエラーを伝えなければなりません。前の結果は、クラッシュか、ほかのリクエストのデータでした。Nginx.logはリクエストを必要としないので、そこではnginxのエラーログに書きます。
    • ドキュメントが示していた挙動:ドキュメントは、リクエストのメソッドを、リクエストのハンドラの中でだけ示しています。上に挙げたコードは、どのリクエストにも属さずに動きます。2.7.0のドキュメントには、Nginx.errloggerについて「Sends an error to the log file」と書いてあり、使えるハンドラを限る記述はありません(2.7.0のNginx.errlogger)。Nginx.logは、Nginx.errloggerのエイリアスです。
  • 影響の範囲:リクエストのデータを使う、init、init worker、exit worker、server context、ハンドシェイクのコードです。このようなコードは、前から確実には動きませんでした。そこでNginx.logの例外に頼っていたコードも、影響を受けます。
  • 必要な対応:リクエストのデータは、リクエストのハンドラの中でだけ使ってください。上に挙げたコードでは、いまはNginx.logを使えます。
  • テスト:test/t/ngx_mruby.rbの、ポート18080でmruby_init_worker_codeを試すテストと、ポート18089でハンドシェイクのハンドラを試すテストです。

add_listenerは、server contextのコードの中でだけ使えます。

  • 変更前:nginxが設定を読み終えた後、たとえばリクエストのハンドラでNginx::Server#add_listenerを呼ぶと、もう有効でない設定のデータが使われました。Nginx::Stream.add_listenerは、streamのセッション(mruby_stream_code)か、streamのinit、init worker、exit workerのコードで呼ぶと、streamモジュールの内部のコンテキストをサーバーの設定と取り違えました。この内部のコンテキストは、いまのセッションを持つものです。その結果、プロセスがクラッシュしました。mruby_stream_server_context_codeが動く間は、Nginx::Stream.logとセッションのメソッドが、サーバーの設定をこの内部のコンテキストと取り違えていました。
  • 変更後:mruby_server_context_handler_codeの外でNginx::Server#add_listenerを呼ぶと、RuntimeErrorが発生します。メッセージはadd_listener can only be used in mruby_server_context_handler_codeです。mruby_stream_server_context_codeの外でNginx::Stream.add_listenerを呼ぶと、RuntimeErrorが発生します。メッセージはadd_listener can only be used in mruby_stream_server_context_codeです。どちらのメソッドも、そのコードの中では、nginxが設定を読む間にこれまでどおり動きます。mruby_stream_server_context_codeでは、streamのinitとworkerのコードと同じく、Nginx::Stream.logはnginxのエラーログに書き、セッションのメソッドでは例外が発生します。
  • 変更の理由:
    • 不具合:「変更前」に書いたとおり、server contextのコードの外でadd_listenerを呼ぶと、もう有効でない設定のデータが使われるか、プロセスがクラッシュしました。mruby_stream_server_context_codeでは、Nginx::Stream.logとセッションのメソッドが、サーバーの設定をstreamモジュールの内部のコンテキストと取り違えていました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:add_listenerには、nginxの設定のパーサが持つ状態が必要です。この状態は、nginxが設定を読む間だけ存在します。HTTPモジュールのNginx::Server#add_listenerは、パースが終わった後にこの状態を使いました。streamモジュールには、内部のコンテキストを置く場所があります。mruby_stream_server_context_codeが動く間、streamモジュールは、この場所にサーバーの設定を入れていました。Nginx::Stream.add_listenerは、呼ばれるたびにこの場所を読みました。それ以外のときにこの場所にあるのは内部のコンテキストで、サーバーの設定ではありません。2.7.1では、そのコードが動く間だけ、サーバーの設定を別の場所に置きます。そのため、add_listenerは、どこで呼ばれたかを判別できます。ほかのメソッドは、常に内部のコンテキストを見つけます。mruby_stream_server_context_codeは、nginxが設定を読む間に、どのセッションにも属さずに動きます。そのため、streamのinitとworkerのコードと同じく、そこではセッションのメソッドで例外が発生し、Nginx::Stream.logはnginxのエラーログに書きます。
    • ドキュメントが示していた挙動:2.7.0のドキュメントは、設定の中のNginx::Server#add_listenerを、mruby_server_context_handler_codeの中でだけ示しています(Nginx::Server#add_listener)。Nginx::Stream.add_listenerの説明は、その例を参照しています(Nginx::Stream.add_listener)。ほかのどこで呼べるかは、書いてありません。mruby_stream_server_context_codeについては、何も書いてありません。
  • 影響の範囲:server contextのコードの外でadd_listenerを呼ぶコードと、Nginx::Stream.logかセッションのメソッドを呼ぶmruby_stream_server_context_codeです。前者のコードは、前から動きませんでした。
  • 必要な対応:add_listenerは、server contextのコードの中でだけ呼んでください。mruby_stream_server_context_codeにはセッションが無いので、そこではセッションのメソッドを呼ばないでください。
  • テスト:ポート18080のテストは、test/t/ngx_mruby.rbで、contentハンドラの中のNginx::Server#add_listenerを試します。ポート12357のテストは、2つ目のnginxで、セッションの中のNginx::Stream.add_listenerを試します。2つ目のnginxはtest/t/cases/_second_instance.rbが起動し、test/t/cases/stream.rbはtest/t/cases/_prelude.rbのsecond_instanceでそれを呼びます。

9. 後のハンドラがmruby_setの変数を上書きしなくなりました

Section titled “9. 後のハンドラがmruby_setの変数を上書きしなくなりました”
  • 変更前:mruby_setかmruby_set_codeがリクエストの中で動いた後、同じリクエストの後のハンドラがNginx::Async.sleepから再開すると、そのハンドラの最後の式が、その変数に代入されました。serverレベルのmruby_setやmruby_set_codeでも、同じでした。
  • 変更後:変数に代入されるのは、そのmruby_setのハンドラ自身の結果だけです(#548)。自分でsleepするmruby_set_codeは、これまでどおり、再開したときに変数を設定します。
  • 変更の理由:
    • 不具合:mruby_setは、変数の名前をリクエストのコンテキストに保存しました。その名前を消す処理はありませんでした。そのため、リクエストの後のハンドラがNginx::Async.sleepから再開すると、ngx_mrubyはそのハンドラの最後の式を変数に書きました。たとえば、後のproxy_passが使う$backendに書きました(#548)。
    • 前の挙動を残せなかった理由:設定には、この書き込みを指示する記述がありません。残すと、関係の無いハンドラでsleepしただけで、変数が変わります。
    • ドキュメントが示していた挙動:mruby_setの変数は、そのmruby_setが設定するものです。ディレクティブのドキュメントには、「called at one of the rewrite phases to set a nginx variable」と書いてあります(2.7.0のmruby_set and mruby_set_code)。
  • 影響の範囲:mruby_set*と、Nginx::Async.sleepを使う後のハンドラを組み合わせるリクエストです。
  • 必要な対応:ありません。後のハンドラで変数を変えたいときは、Nginx::Var.new.backend = "..."のように、明示的に代入してください。
  • テスト:test/t/cases/async_set_var_target.rbのポート18123のテストです。

10. httpブロックの無い設定でもnginxが起動します

Section titled “10. httpブロックの無い設定でもnginxが起動します”
  • 変更前:ngx_mrubyを組み込んだnginxで、streamだけの設定のようにhttp {}ブロックの無い設定を使うと、起動時にすべてのワーカーがクラッシュしました。nginxは、新しいワーカーを起動し続けました。
  • 変更後:nginxが起動します(#536)。
  • 変更の理由:
    • 不具合:ワーカーの起動と終了のときに動くHTTPモジュールのフックは、HTTPモジュールのメインの設定があるかを確かめずに、その設定を読んでいました。http {}ブロックが無いとメインの設定も無く、ワーカーがクラッシュしました(#536)。
    • 前の挙動を残せなかった理由:nginxは、http {}の無い設定を受け付けます。ngx_mrubyのstreamモジュールは、既にこの場合を扱っていました。前の挙動はクラッシュなので、残すものはありません。
    • ドキュメントが示していた挙動:http {}ブロックの無い設定については、ドキュメントに何も書いてありませんでした。
  • 影響の範囲:ngx_mrubyのHTTPモジュールを組み込んだかロードしたnginxで、streamだけの設定のようにhttp {}ブロックの無い設定です。
  • 必要な対応:ありません。
  • テスト:test/t/ngx_mruby.rbのポート12399のテストです。

11. mrubyのヘッダフィルタの後でNginx.rputsを呼ぶボディフィルタで、「header already sent」がログに出なくなりました

Section titled “11. mrubyのヘッダフィルタの後でNginx.rputsを呼ぶボディフィルタで、「header already sent」がログに出なくなりました”
  • 変更前:mrubyのヘッダフィルタと、コードがNginx.rputsかNginx.echoを呼ぶmrubyのボディフィルタのあるlocationでは、nginxがリクエストごとに[alert] header already sentをログに出しました。また、ボディフィルタは、ヘッダを送った後でステータスを200にしました。ドキュメントには、ボディフィルタでのこの呼び出しをサポートしないと書いてあります。クライアントはnginxが送ったステータスを受け取りましたが、logフェーズには200が見えていました。logハンドラとlog_formatの$statusは、404のレスポンスでも200を読みました。
  • 変更後:nginxはこのアラートをログに出さず、logフェーズには、nginxが送ったステータスが見えます。レスポンスは前と同じです(#563)。ただし、ボディフィルタがボディの長さを変える場合は、レスポンスが変わります。この場合は、項目2の4つ目の部分に書きました。
  • 変更の理由:
    • 不具合:ボディフィルタは、コードの最後でNginx.rputsの出力を送るときに、応答するハンドラのためのコードパスを使っていました。このコードパスでヘッダが2回目に送られ、ステータスが200になりました(#563)。
    • 前の挙動を残せなかった理由:このコードパスを残すと、「変更前」に書いたとおり、リクエストごとに[alert] header already sentの行が出続けます。また、logフェーズで見えるステータスが、nginxが送ったステータスと違ったままになります。locationにmrubyのヘッダフィルタがあるときは、ボディフィルタはもうこのコードパスを通りません。ボディフィルタでのNginx.rputsの出力は、前と同じく破棄します。
    • ドキュメントが示していた挙動:ドキュメントには、Nginx.rputsとNginx.echoは出力のボディフィルタでは使えないと書いてあります。その記述は「You can NOT use Nginx.rputs and Nginx.echo in the output body handlers」です(2.7.0のNginx::Filter Class)。logフェーズが見るステータスについては、何も書いてありませんでした。
  • 影響の範囲:mruby_output_header_filterかその_codeの形と、mruby_output_body_filterかその_codeの形があり、ボディフィルタがNginx.rputsかNginx.echoを呼ぶlocationです。それらのリクエストの$statusかheader already sentの行を読む、アクセスログ、logハンドラ、アラートも影響を受けます。
  • 必要な対応:ログの統計かアラートが、それらのリクエストのステータス200かheader already sentの行に頼っている場合を除き、ありません。
  • テスト:test/t/cases/output_filters_with_content_handler.rbのポート18138のテストです。

12. 応答の前に、読まれていないリクエストボディを破棄します

Section titled “12. 応答の前に、読まれていないリクエストボディを破棄します”
  • 変更前:ハンドラがリクエストボディを読まずにNginx.rputsかNginx.echoで応答すると、ボディが接続に残りました。たとえば、mruby_enable_read_request_bodyなしでPOSTに応答するcontentハンドラやrewriteハンドラが、これに当たります。nginxが次のリクエストのために開いておく接続、つまりHTTP/1.1のkeep-aliveの接続では、nginxはそのボディを次のリクエストの始まりとして読みました。
  • 変更後:ngx_mrubyは、そのようなハンドラの応答を送る前に、どのハンドラも読んでいないリクエストボディを破棄します。nginxも、return、エラーページ、静的ファイルのような自分のレスポンスの前に、同じようにボディを破棄します。接続の次のリクエストは、クライアントがボディの後に送ったリクエストになります。ボディを破棄できない場合は、nginxは応答の後に接続を閉じます。Nginx::Async.sleepの後でrewriteハンドラが送る応答も、同じです。ハンドラが読んだボディは、影響を受けません。
  • 変更の理由:
    • 不具合:nginxは、リクエストに応答するハンドラが、先にリクエストボディを読むか破棄すると想定しています。開いたままの接続では、次のリクエストがボディの後から始まるからです。nginx自身のハンドラは、これをしています。ngx_http_send_response、エラーページ、staticモジュールがそうです。mrubyのハンドラの応答は、これをしていませんでした。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:「変更前」に書いたとおり、接続に残ったボディは、次のリクエストの始まりとして読まれます。ボディを破棄すれば、ハンドラが必要としないボディのために応答を待たせずに、接続を使える状態に保てます。
    • ドキュメントが示していた挙動:どのハンドラも読まないリクエストボディについては、ドキュメントに何も書いてありませんでした。
  • 影響の範囲:POSTやPUTのようにボディのあるリクエストに、ボディを読まずにNginx.rputsかNginx.echoで応答するハンドラのうち、nginxが次のリクエストのために開いておく接続で応答するものです。
  • 必要な対応:ありません。ボディが必要なハンドラは、これまでどおりmruby_enable_read_request_body onでボディを読みます。
  • テスト:test/t/cases/unread_request_body.rbのポート18151のテストです。contentハンドラ、rewriteハンドラ、Nginx::Async.sleepの後で応答するrewriteハンドラを試します。

13. contentハンドラが出力なしで終わると、レスポンスは500になります

Section titled “13. contentハンドラが出力なしで終わると、レスポンスは500になります”
  • 変更前:メインリクエストのmruby_content_handler*が出力なしで終わり、ステータスを設定していないか、200、201、204以外の300未満のステータスを設定していると、レスポンスが送られませんでした。ステータスを設定していない場合とは、何も書かない場合、Nginx.rputsで""かnilだけを書く場合、レスポンスヘッダだけを設定する場合です。300未満のステータスの例はNginx::HTTP_ACCEPTEDです。Nginx::Async::HTTP.sub_requestが再開した後にこのように終わった場合も、同じでした。nginxは、HTTP/1.0の接続を1バイトも送らずに閉じ、HTTP/1.1のkeep-aliveの接続を、keepalive_timeoutまでレスポンスなしで開いたままにしました。keepalive_timeoutのデフォルトは75秒です。そのため、クライアントは自分のタイムアウトまで待ちました。
  • 変更後:そのようなリクエストには500が返ります。そのボディはnginxのエラーページか、500に設定したerror_pageで、ヘッダにはConnection: closeが付きます。これは、出力なしでNginx.return Nginx::HTTP_OKを呼んだ場合に、2.7.0でも既に返っていた応答と同じです(#566)。error.logには、errorレベルの行が出ます。その行は、status code is not set, and response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORか、status code is <N>, but response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORです。
  • 変更の理由:
    • 不具合:nginxは、201、204、300以上のステータスには、自分でレスポンスを送ります。しかし、ステータスが300未満で201と204以外のときと、ステータスが設定されていないときは、何も送らずにリクエストを終えます。出力の無いハンドラは、そのような値をそのままnginxに渡していました。レスポンスの無いリクエストは、不具合です(#225)。クライアントは自分のタイムアウトまで待たされ、keep-aliveの接続が無駄に占有されます。
    • 前の挙動を残せなかった理由:残すと、これらのリクエストはレスポンスの無いままになります。2.x系列では、これらのリクエストに、出力なしのNginx::HTTP_OKに既に返していたのと同じ応答を返します。そのため、2.7.0でレスポンスを受け取っていたリクエストが、違うレスポンスを受け取ることはありません。
    • ドキュメントが示していた挙動:出力なしで終わるcontentハンドラについては、ドキュメントに何も書いてありませんでした。Nginx.returnについては、「Returns nginx status code to send back HTTP status to the client」と書いてあります(2.7.0のNginx.return)。2.7.1では、202のようなステータスを出力なしで返すと、500になります。2.7.0でも、出力なしの200は、既に500になっていました。
  • 影響の範囲:出力なしで終わることのあるmruby_content_handler*のハンドラと、これらの行を見るログの監視です。ハンドラの例は、条件が成り立つときだけ書くハンドラや、空かnilになりうる値をNginx.rputsに渡すハンドラです。status code is 200, but response body is emptyに一致させるパターンは新しい行に一致せず、response body is emptyに一致させるパターンでは、新しい行も数に入ります。
  • 必要な対応:空のボディで応答すべきハンドラは、Nginx.return Nginx::HTTP_NO_CONTENTを呼ぶか、ボディを書いてください。Nginx::HTTP_NO_CONTENTの場合は、nginx自身が204で応答します。拒否するつもりのハンドラは、Nginx::HTTP_FORBIDDENやNginx::HTTP_UNAUTHORIZEDのように、意図したステータスを返してください。status code is 200, but response body is emptyに一致させるログの監視は、新しい行も見えるように、response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORに一致させてください。
  • このリリースで変わらない挙動:出力なしのNginx::HTTP_OKは、前と同じく500です。201、204、300以上のステータスは、nginx自身が応答します。出力のあるハンドラ、Nginx.redirect、ほかの種類のハンドラ、サブリクエストも変わりません。Nginx::Async::HTTP.sub_request、SSIのinclude、auth_requestには、前と同じサブリクエストのステータスが見えます。出力を書いてから、200、201、204以外の300未満のステータスを設定するcontentハンドラは、これまでどおりレスポンスを送りません。たとえば、Nginx.rputs "x"の後にNginx.return Nginx::HTTP_ACCEPTEDを呼ぶ場合です。この件は、「既知の制限」の節に書きました。
  • テスト:test/conf/conf.d/54-content-empty-body.confとtest/t/cases/content_empty_body.rbの、ポート18136のテストです。

14. nginxがボディの読み込みを打ち切った後は、待っていたハンドラが動かなくなりました

Section titled “14. nginxがボディの読み込みを打ち切った後は、待っていたハンドラが動かなくなりました”

mruby_enable_read_request_body onのとき、post-read、server rewrite、rewrite、access、contentのハンドラは、リクエストボディの残りを待ちます(項目6)。nginxは、ボディの残りを受け取らずに、その読み込みを打ち切ることがあります。それは、client_body_timeoutが切れたとき、クライアントが接続を閉じたとき、nginxがボディに400か413で応答したときです。400で応答する例は、不正なchunkedのボディです。この項目では、そのようなリクエストでその後に動く、mrubyのlogハンドラとmrubyの出力フィルタについて書きます。

  • 変更前:待っていたハンドラがpost-readハンドラかaccessハンドラのとき、そのようなlogハンドラやフィルタは、自分でボディの読み込みを始めました。その読み込みによって、待っていたハンドラのコードが、nginxが既に応答を決めた後に動きました。このコードは、それまで動いていませんでした。client_body_timeoutの後や、クライアントが接続を閉じた後は、accessハンドラのコードがlogフェーズの間に動きました。不正なchunkedのボディの後は、nginxが400エラーページを送る間にそのコードが動きました。Content-Lengthはページ1回分の長さなのに、レスポンスにはページが2回入りました。Content-Lengthは157バイトで、送ったのは314バイトでした。待っていたハンドラがserver rewrite、rewrite、contentのハンドラのときは、2.7.0ではリクエストがまったく終わりませんでした(項目6)。そのため、このlogハンドラやフィルタは動きませんでした。
  • 変更後:logハンドラやフィルタは、読み込みを始めずに、すぐに動きます。Nginx::Request#bodyは、ボディのうち、nginxが読み込みを打ち切る前に読んだ部分を返します。その部分がnginxのボディのバッファ(client_body_buffer_size)に収まらず、nginxが一時ファイルに書いていた場合は、nilを返します。error.logには、項目6のinfoの行が出ます。その行はthe request body is not complete and a log handler cannot wait for it, so it runs without the bodyで、フィルタでは、a log handlerの部分がa header filterかa body filterになります。待っていたハンドラのコードは動きません。client_body_timeoutの後は、nginxはレスポンスなしで接続を閉じ、アクセスログには408が記録されます。クライアントが接続を閉じた後は、アクセスログに400が記録されます。不正なchunkedのボディの後は、クライアントはnginxの400エラーページを1回だけ受け取り、nginxは接続を閉じます。
  • 変更の理由:
    • 不具合:リクエストボディの読み込みが既に始まっているとき、nginxは、新しい読み込みの完了ハンドラをすぐに呼びます。ngx_mrubyの完了ハンドラは、待っているハンドラのために、リクエストのフェーズを動かし直します。そのため、nginxが最初の読み込みを打ち切った後に読み込みを始めたlogハンドラやフィルタは、logフェーズから、またはレスポンスの途中から、それらのフェーズを動かし直しました。この不具合は、セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてあります。
    • 前の挙動を残せなかった理由:nginxが打ち切った読み込みは完了しないので、待っているハンドラには、動くためのボディがありません。また、nginxは既にリクエストに応答したか、リクエストを閉じています。そのときにそのハンドラのコードを動かすと、終わったリクエストのために、アクセスチェックやログの行のような処理をし、nginxのエラーページを2回目に送りました。logハンドラやフィルタはボディを待てないので(項目6)、自分で読み込みを始めずに、nginxが読んだもので動きます。
    • ドキュメントが示していた挙動:2.7.0のこのディレクティブのドキュメントには、ngx_mrubyが「reads a HTTP request body before calling mruby code」と書いてありました(mruby_enable_read_request_body)。nginxがボディ全体を読まずに打ち切る読み込みについては、何も書いてありませんでした。
  • 影響の範囲:mruby_enable_read_request_body onの設定のうち、post-readハンドラかaccessハンドラがボディを読み、同じリクエストでmrubyのlogハンドラかmrubyの出力フィルタも動くものです。対象は、nginxがボディの読み込みを早く打ち切るリクエストで、client_body_timeout、閉じた接続、ボディへの400か413の応答がこれに当たります。
  • 必要な対応:ありません。ボディを待つハンドラのコードは、ボディ全体を受け取って1回だけ動きます。ボディがそろう前にnginxが読み込みを打ち切ったときは、動きません。
  • テスト:test/t/cases/request_body_later.rbのポート18152のテストです。待っているrewriteハンドラについて、client_body_timeoutの後のlogハンドラと、不正なchunkedのボディを受けた後のヘッダフィルタとボディフィルタを試します。

ありません。

セキュリティアドバイザリGHSA-h7fw-wf49-8rv5に書いてある修正:

  • ボディフィルタは、ボディをNginx.rputsかNginx.echoで書いた場合も、locationにmrubyのヘッダフィルタもある場合も、自分の出力のとおりにボディを送ります(項目2)。
  • ヘッダフィルタの中のNginx::Filter#bodyは、レスポンスのボディでないバイトを返さなくなりました。ボディフィルタの外でNginx::Filter#body=と#outputを呼んでも、後にボディフィルタが続くときに、ワーカーがクラッシュしなくなりました(項目2)。
  • accessハンドラかpost-readハンドラがNginx.rputsの後で例外を投げると、nginxはcontentハンドラへ進まず、500でリクエストが止まります。mruby_set*のハンドラかヘッダフィルタで例外が発生した後は、破棄した出力がContent-Lengthに入らなくなりました(項目3)。
  • mruby_enable_read_request_bodyを使う場合について、次のように直しました。ハンドラが始まった後にリクエストボディが届くserver rewrite、rewrite、contentのハンドラは、ボディを受け取って動きます。遅れて届いたボディの後でも、auth_requestのような後のフェーズのハンドラが再開します。クライアントが接続を閉じたときやclient_body_timeoutが切れたときは、nginxがリクエストを開いたままにせずに終えます(項目6の1つ目の部分)。
  • ハンドシェイクのハンドラの外でNginx::SSLを使うと、ワーカーがクラッシュしたり意味の無い値が返ったりせずに、RuntimeErrorが発生します(項目7、issue #339)。
  • コードがNginx::SSL.newを呼ばない後のハンドシェイクは、前のハンドシェイクの証明書を受け取らなくなりました(項目7)。
  • リクエストやセッションの外で、リクエストスコープのメソッドとstreamのセッションのメソッドを呼ぶと、クラッシュしたり、ほかのリクエストのデータを使ったりせずに、RuntimeErrorが発生します。mruby_server_context_handler_codeの外でNginx::Server#add_listenerを呼ぶと例外が発生し、リクエストの外ではNginx.logがnginxのエラーログに書きます(項目8)。
  • mruby_stream_server_context_codeの外でNginx::Stream.add_listenerを呼ぶと、streamモジュールの内部のコンテキストをサーバーの設定として使わずに、例外が発生します(項目8)。
  • Nginx.rputsで応答しても、読まれていないリクエストボディが接続に残らなくなりました(項目12)。
  • nginxがボディ全体を受け取らずに読み込みを打ち切った後に動くlogハンドラや出力フィルタは、ボディを待っていたハンドラのコードを動かさなくなりました。nginxは、ボディに対するエラーページを1回だけ送ります(項目14)。

そのほかの修正:

  • logハンドラと出力フィルタでは、Nginx::Asyncでタイマーや中断したハンドラが残らずに、例外が発生します(#551、項目4)。
  • Nginx::Asyncがハンドラを再開した後に例外が発生すると、500が返り、ワーカーの次のリクエストは失敗しなくなりました。その間にnginxがキューへ入れたposted requestsも動きます(#550、項目5)。
  • mruby_enable_read_request_bodyを使うとき、mruby_set*、logハンドラ、フィルタは、ボディの読み込みを残さなくなりました(項目6の2つ目の部分)。
  • 再開したハンドラは、前のmruby_setの変数を上書きしなくなりました(#548、項目9)。
  • http {}ブロックの無い設定でも、nginxが起動します(#536、項目10)。
  • 出力なしで終わるcontentハンドラは、クライアントをレスポンスの無いままにせず、500を返します(#566、#225の修正、項目13)。
  • mrubyのヘッダフィルタと、Nginx.rputsかNginx.echoでレスポンスを書くmrubyのハンドラのあるlocationで、ワーカーがクラッシュしなくなりました。そのハンドラの例は、contentハンドラやrewriteハンドラです。ヘッダフィルタは1回だけ動きます(#563、#206の修正)。
  • Nginx.rputsかNginx.echoを呼ぶヘッダフィルタで、ワーカーがクラッシュしなくなりました(#563)。ただし、この呼び出しは、引き続きサポートしていません。nginxのreturnのように、Nginx.rputsで書くmrubyのハンドラ以外がレスポンスを作った後では、レスポンスにその出力は入りません。一方、mrubyのボディフィルタの無いlocationでは、その出力がContent-Lengthに入り、locationのlogハンドラがレスポンスの後にその出力を送ります。Nginx.rputsで書くmrubyのcontentハンドラの後では、その出力はそのハンドラの出力に足され、Content-Lengthにも入ります。その後で例外を投げるヘッダフィルタは自分の出力だけを破棄し、mrubyのcontentハンドラの出力は、そのContent-Lengthとともにレスポンスに残ります(項目3)。
  • Nginx::SSLの証明書のセッターは、NULバイトを含む値も、最後までコピーします。
  • 失敗したNginx::Async::HTTP.sub_requestは、ワーカーが終了するまでハンドラのFiberをガベージコレクタに登録したままにしなくなりました(#547)。
  • 同じ種類の後のディレクティブがstreamのディレクティブを置き換えるとき、置き換えられたコードが使っていたmrubyのコンパイラコンテキストが、nginxが設定を読む間にリークしなくなりました(#540)。
  • RubyからServerヘッダを設定するとき、ゼロで初期化したヘッダの要素を確保するようになりました(#530)。

このリリースでは、2.7.0の次の挙動を変えていません。

  • contentハンドラでNginx::Async.sleepを呼ぶと、レスポンスなしでリクエストが終わります。mruby_content_handler*は、例外なしでNginx::Async.sleepを呼べます(項目4)。しかし、nginxはsleepするcontentハンドラを待ちません。リクエストはsleepの位置で終わり、クライアントにはレスポンスが返らず、sleepの後のコードは動きません。ドキュメントには、Nginx::Asyncが動く場所として、set、rewrite、accessのハンドラが挙げてあります。代わりに、rewriteハンドラかaccessハンドラでsleepしてください。rewriteハンドラなら、sleepの後でレスポンスを書くこともできます。後のリリースで、contentハンドラが待てるようにする予定です。
  • 出力を書くaccessハンドラやpost-readハンドラでは、リクエストが先へ進みます。Nginx.rputsかNginx.echoで書き、例外もエラーのステータスもなしで終わるaccessハンドラやpost-readハンドラでは、ハンドラが終わるときに、その出力がレスポンスとして送られます。続いて、nginxは後のフェーズへ進みます。locationのcontentハンドラかnginxのstaticモジュールも動き、nginxはheader already sentをログに出します。contentハンドラが書いたものは、Content-Lengthを超えてレスポンスの後に続きます。これらのハンドラでは、出力を書かないでください。リクエストを拒否するには、Nginx.return Nginx::HTTP_FORBIDDENのようにエラーのステータスを返し、応答はcontentハンドラで書いてください。3.x系列では、各フェーズのハンドラがレスポンスを終える方法を、設計し直す予定です。
  • 出力の後に、200、201、204以外の300未満のステータスを設定すると、レスポンスが送られません。Nginx.rputsかNginx.echoで出力を書いてから、たとえばNginx.return Nginx::HTTP_ACCEPTEDで、このようなステータスを設定するcontentハンドラが、これに当たります。nginxは、HTTP/1.0の接続を1バイトも送らずに閉じ、HTTP/1.1のkeep-aliveの接続を、レスポンスなしで開いたままにします。レスポンスにボディがあるときはNginx::HTTP_OKで、無いときはNginx::HTTP_NO_CONTENTで応答してください。3.x系列では、どのステータスでもボディを返せるようにする予定です。
  • デフォルトのnginx:build.shとtest.shが使うnginx_versionは1.31.6です。2.7.0では1.27.2でした。
  • CIは、nginx 1.31.6、1.30.5、1.28.3、1.26.3のそれぞれで、ngx_mrubyを静的モジュールと動的モジュールとしてテストします。OpenSSLは、ランナーに共有ライブラリとして入れた1.1.1wと、nginxに組み込んでコンパイルした1.1.1wの両方を使います。さらに、AddressSanitizerとUndefinedBehaviorSanitizerを有効にした1.31.6のビルドを1つ加えます。CIはプルリクエストで動き、ci-okのチェックを1つ報告します。2.7.0では、1.27.2と1.26.2をテストしていました。
  • CI:ルールセットが必須にしているステータスチェックのci-okは、新しいジョブfilesの成功も必要とするようになりました。filesのジョブは、リポジトリに置かないファイルをプルリクエストが追加すると失敗します(#578)。このジョブが動かすtest/check_added_files.shは、変更が追加するファイルと、変更のコミットのどれかが追加するファイルが、test/forbidden_paths.txtのパターンにマッチするかを調べます。パターンには、.claude/settings.json、.claude/commands/、.claude/skills/を除く.claude/の中身、build/やbuild_dynamic/のようなトップのビルドディレクトリ、mruby/build/、./configureかMakefileがトップに書くファイル、.logのファイル、.envと.env.*、秘密鍵、エディタとOSが残すファイル、コアダンプなどがあります。変更が追加する空のファイルも、名前が.gitkeepか.keepでなければ失敗します。master、next、v*.xへのpushでは、このジョブはpushしたコミットを調べます。このジョブはリポジトリの履歴全体を必要とします。チェックの内容と、プルリクエストを開く前に手元で動かす方法は、docs/test/README.mdに書いてあります。
  • build_config.rbは、mruby-redisをビルドしなくなりました(#528、項目1)。
  • build.sh:OPENSSL_SRC_VERSIONを設定すると、ダウンロードしたOpenSSLのソースディレクトリを、絶対パスでconfigureに渡します。そのため、OpenSSLの./configは、必要とする絶対パスの--prefixを受け取ります。2.7.0では、Directory given with --prefix MUST be absoluteで止まっていました(#562、#519の修正)。build.shかtest.shのコマンドラインで指定した--with-openssl-srcは、これまでどおり、そのままconfigureに渡します。
  • test.sh:HTTPのテストサーバーは、58xxxの代わりに、エフェメラルポートの範囲より下の18080-18090、18101-18103、18110-18131、18136、18138、18143-18144、18147-18152で待ち受けます。streamのサーバーは12345-12359を使い、1つのテストが12399を使います。また、test.shは、test/conf/conf.dの設定の断片を読み込み、test/t/casesのケースのファイルを実行します。この仕組みは、テストのドキュメントに書いてあります。nginxをビルドするときに追加するコンパイラとリンカのフラグは、NGINX_EXTRA_CC_OPTとNGINX_EXTRA_LD_OPTから受け取ります。nginxのバージョンは全体で比べるので、nginx 1.11.5以降のビルドには、パッチ番号に関わらずstream accessモジュールが入ります。項目6のテストのために、nginxを--with-http_auth_request_module付きでconfigureします。前のtest.shが作ったビルドディレクトリには、もう一度configureするまで、前のconfigureのオプションが残ります。この点は、テストのドキュメントに書いてあります。
  • リポジトリに、セキュリティポリシー(SECURITY.md)、コントリビューターとエージェント向けの指示(AGENTS.md)、v2.xブランチができました。v2.xブランチは、v3がmasterに昇格するまで、masterをミラーします。

2026-10-14に、nextブランチにある、callgrindで命令数を比べるスクリプトで測りました。使ったのは570cdab05のtest/perf/compare.shで、手順は同じブランチのdocs/test/README.mdの「Performance comparison with callgrind」にあります。両方のツリーを、nginx 1.31.6と21個のサードパーティのmrbgemsでリリースビルドしました。mrbgemsは、両方のツリーで同じものを同じコミットで使いました。環境は、aarch64のマシンで動くコンテナのUbuntu 22.04で、gccは11.4、valgrindは3.18.1です。各シナリオで、ウォームアップとして2000回のリクエストを送った後に、20000回のリクエストを送りました。ベースはb3df70fのmasterで、2.7.1の修正とマージした2.xのコミットです。そのツリーは4e88dcc47ecaです。ヘッドはb3df70fに2.7.1の修正を加えたもので、ツリーは3ab6c7888a35、src/のツリーは8b6857bd850eです。次の表に、mrubyのガベージコレクタの分を除いて、nginxが1リクエストあたりに実行する命令数を示します。

シナリオ b3df70f 2.7.1 変化率
hello 11159 11297 +1.23%
headers 34154 34286 +0.39%
var 19837 19976 +0.70%
filter 13229 13308 +0.60%
sleep 14719 14852 +0.90%
sub_request 47443 47617 +0.37%
file 13196 13335 +1.05%
proxy_plain_2k 21952 21992 +0.18%
proxy_plain_64k 22262 22301 +0.18%
proxy_stream_plain_50 54301 54348 +0.09%
auth 46237 46299 +0.13%
route_json_2k 106148 106224 +0.07%
ruby_call_1 10736 10799 +0.59%
ruby_call_10 35215 35557 +0.97%

どのシナリオの変化率も、比較のスクリプトが警告を出す3%を下回りました。相対の変化が最も大きいのはhelloで、+1.23%、1リクエストあたり約138命令です。絶対の変化が最も大きいのはruby_call_10で、10回のmruby_set_codeの呼び出しで1リクエストあたり342命令、1回あたり約34命令です。修正では、リクエストごとにメモリを確保する処理を自前では足しておらず、ブロッキングの待ちも足していません。リクエストボディの残りが必要なハンドラは、nginxのNGX_DONEで待ち、nginxがボディを読んだときにもう一度動きます。Nginx.rputsの後で発生する例外に対する修正では、ハンドラが動く前に、Nginx.rputsのチェーン、その最後のリンク、レスポンスの長さを記録します。Nginx.rputsがチェーンを始めるときにも、長さをもう一度記録します。例外の後には長さを元に戻し、ヘッダフィルタではチェーンも元に戻します。メモリを確保しうる新しい呼び出しは、nginx自身のngx_http_discard_request_bodyだけで、どのハンドラも読まなかったchunkedのリクエストボディのために呼びます。

サポートするnginxのバージョンは、README.mdに書いたとおり、nginxのmainlineとstableの系列です。CIは、「ビルドとテストの変更」の節に挙げたバージョンをビルドします。

  • 追加、改名、削除したディレクティブはありません。このリリースで、モジュールをビルドし直してください。2.7.0をビルドしたときと同じ方法で、ngx_mruby 2.7.1を組み込んだnginxをビルドし、いつもどおりバイナリか動的モジュールを置き換えます。Nginx.module_versionは2.7.1を返します。
  • mrubyは3.3.0のままで、ngx_devel_kitも変わりません。
  • build_config.rbのサードパーティのmrbgemsは、2.xでは、これまでどおりバージョンを固定せずに、それぞれのリポジトリから取得します。そのため、新しいビルドには、2.7.0のビルドより新しいバージョンのmrbgemsが入ることがあります。Redisには、項目1の変更が必要です。

アップグレードの前に、次の点を確かめてください。各行は、上の各項目の「必要な対応」です。ここに無い項目では、変更は要りません。

  • Redis:ビルドの前に、build_config.rbのconf.gem github: 'matsumotory/mruby-redis'のコメントアウトを外してください(項目1)。
  • 出力フィルタ:ボディフィルタからNginx.rputsとNginx.echoを取り除いてください。ヘッダフィルタでは、レスポンスの長さをNginx::Var.new.sent_http_content_lengthで読んでください。ボディの設定と読み取りは、mruby_output_body_filter*の中だけにしてください(項目2)。
  • 出力の後のraise:raiseは、エラーのためだけに使ってください。contentハンドラを途中で終えるにはNginx.return Nginx::HTTP_OKとreturnを使い、accessハンドラとpost-readハンドラでは出力を書かないでください(項目3)。
  • logハンドラか出力フィルタの中のNginx::AsyncかFiber.yield:rewriteハンドラかaccessハンドラ、またはmruby_set_codeの中で待ち、結果をnginxの変数で渡してください(項目4)。
  • Nginx::Asyncの後の例外で500以外を返すべきハンドラ:ハンドラの中で例外をrescueし、そこでステータスを設定してください(項目5)。
  • リクエストボディが必要なmruby_set*、logハンドラ、出力フィルタ:serverレベルのmruby_post_read_handler_codeのような待てるハンドラで、先にボディを読んでください(項目6)。
  • Nginx::SSL:ハンドシェイクのハンドラの中でだけ呼んでください。一部のハンドシェイクでだけ証明書を設定し、ほかのハンドシェイクでNginx::SSL.newを呼ばないハンドラでは、いまは、そのほかのハンドシェイクがssl_certificateを受け取ります(項目7)。
  • リクエストの外で動くコードで呼ぶリクエストのメソッド:対象のコードは、項目8に挙げたものです。リクエストのデータは、リクエストのハンドラの中でだけ使ってください。add_listenerは、server contextのコードの中でだけ呼んでください。mruby_stream_server_context_codeでは、セッションのメソッドを呼ばないでください(項目8)。
  • mrubyのヘッダフィルタの後でNginx.rputsを呼ぶボディフィルタについて、ステータス200かheader already sentの行に頼るログの統計やアラート:それらを更新してください。logフェーズには、nginxが送ったステータスが見えるようになり、nginxはその行をもうログに出しません(項目11)。
  • 出力なしで終わることのあるcontentハンドラ:Nginx::HTTP_NO_CONTENTを返すか、ボディを書いてください。status code is 200, but response body is emptyに一致させるログの監視も、更新してください(項目13)。