Railsはどうやって500やdebug用のエラー画面を出しているのか

関心

素の 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 している。なるほど。

More