関心
素の Rack middleware はただの玉ねぎ構造で、Exception は Exception で rescue しなければそのまま一番外側の middleware まで伝播して、ユーザーに適切に status code を伝えることなく果ててしまうはずである。
しかし実際には Rails などのフレームワークを使った場合、それっぽいエラー画面をユーザーに伝えることができるようになっている。
それでいて ExceptionNotification gem など、自分で use した middleware の中ではやはり例外を例外のまま拾うことができる。
ということは、通常の
Rack::Builder.new do
use ..
use ..
run ..
end
とは異なる何らかの middleware 群がフレームワークによって外側に配置されているはずである。
結論
予想通り、いい具合に例外を適切な response に変換する middleware が外側の stack にいる。したがって通常のアプリや独自の rack middleware を書く際に例外は例外のまま投げっぱなしでよい。
いざコードリーディング
※ ちょっと事情があって古い Rails を読んでいるので最新のものでは細かい部分で食い違いがあるかもしれないが、基本的な構造は変わっていないはず
railtie の中で middleware を探していくと Rails::Engine にそれっぽい記述が見つかる。
- Rails::Engine の中で default_middleware_stack を呼んでいて
- これが ActionDispatch::MiddlewareStack.new して…途中すっ飛ばして
- Rails::Application::DefaultMiddleware#build_stack の中でデフォルトで定義済みの middleware を use していく。これが
https://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack
に挙げられているもの。
この中で中盤くらいに ActionDispatch::ShowExceptions という middleware があって、こいつに ActionDispatch::PublicExceptions という別な middleware を与えてある。
default_middleware_stack.rb の以下のコードがそれである。
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
通常の Rack::Builder で use している場合の Middleware.new(app, *args, &block ) とは別な形でもう一つの middleware が与えられていて、 rescue Exception した際にはこの show_exceptions_app として PublicExceptions が呼ばれるようになっている。該当部分は以下の通り。
show_exceptions.rb の中身はこう。
def initialize(app, exceptions_app)
@app = app
@exceptions_app = exceptions_app
end
def call(env)
@app.call(env)
rescue Exception => exception
if env['action_dispatch.show_exceptions'] == false
raise exception
else
render_exception(env, exception)
end
end
そして PublicExceptions の中では
def render_html(status)
path = "#{public_path}/#{status}.#{I18n.locale}.html"
path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, 'text/html', File.read(path))
else
[404, { "X-Cascade" => "pass" }, []]
end
end
こんな風に決め打ちで public/ 以下からerror 用の HTML を render している。なるほど。