BetterErrorsのコードリーディングをしました。

基本的にはRackミドルウェアでエラーハンドリングをして、エラーのバックトレースを表示。binding_of_callerがある場合はWeb上からirbでデバッグできるようにJSからRackミドルウェア向けにAPIを叩いてコマンドを実行・結果出力する、という感じです。

エラー表示するまで

Railtieでは、productionではなくconsider_all_requests_localがtrueの場合のみBetterErrors::Middlewareを差し込みます。

allow_ip!ではBetterErrorsでエラーページ表示するクライアントのIPアドレスを設定しています。デフォルトで許可されているのはローカルネットワーク内だけで、dockerやvagrantなどの仮想環境で動かす場合は明示的にallow_ip!を呼び出す必要があります。

better_errors_callが実際の処理になります。caseの上の2つはBetterErrorsのAPIになります。Railsアプリに対してリクエストが来た場合はprotected_app_callが呼ばれます。

protected_app_callは@app.callを呼びつつrescueでエラーハンドリングを行っています。show_error_pageがBetterErrorsのエラーページを表示するメソッドです。

@error_pageに入る値はErrorPageのインスタンスです。

ErrorPage#renderはERBのテンプレートをErrorPageオブジェクトのコンテキストでレンダリングしています。

templates/main.erbは一部を抜粋するとこんな感じです↓

exception_type、request_path、backtrace_framesなどは全てErrorPageのメソッドです。

インタラクティブな処理

templates/main.erbにはフレームを選択したりREPLでRubyを実行するなどのインタラクティブな処理をするためにJavaScriptで処理が書かれています。これらの処理ではBetterErrorsのAPIを呼び出しています。

パスが /__better_errors/{OID}/{method} となっているのがAPIです。以下再掲↓

(ちなみに/__better_errors/のパスでアクセスすると直近のエラー画面を表示することができます)

internal_callが実際の処理でErrorPage#do_variables, ErrorPage#do_evalが呼び出されます。

variablesの方はフレームを切り替えたときに、そのフレームのローカル変数を画面表示するための処理で、evalはREPLの処理です。いずれも各フレームでのbindingが必要になるので、binding_of_callerを利用しています。

binding_of_callerが存在している場合、BetterErrors::ExceptionExtensionを読み込みます。Module#prepend_featuresによってExceptionに#set_backtraceと#__better_errors_bindings_stackが定義されます。

これらのメソッドが定義されている場合、RaisedExceptionの@backtraceに各フレームのバインディングがStackFrameのインスタンス変数にセットされます。

また、@error_pageはRaisedExceptionやenvを保持しているBetterErrors::Middlewareのインスタンス変数です。ミドルウェアのインスタンス変数は同一プロセス内では同じ値を保持することになるので、APIを叩くときには直近で発生したエラーの@error_pageを使っています。

そのため

  1. Aの画面でエラー画面を表示
  2. Bの画面でエラー画面を表示
  3. Aの画面でREPLを動かす

といったことをするとAの画面でBのエラー内容を参照してしまうことになるので、BetterErrorsでは以下のようにErrorPageのidの突き合わせを行うことでエラーハンドリングをしています。