背景
Ruby on RailsでAPIサーバーを実装するとき、予期しないバグでアプリケーションに例外が起きたときに、500エラーレスポンスで固定のメッセージを返したいときがあります。
ApplicationController
に、次のような関数呼び出しを書いて、アプリケーションで共通の例外処理を実装するでしょう。
rescue_from StandardError, with -> do
render json: { message: 'Something wrong' }, status: :internal_server_error
end
問題
問題1: 開発中も例外が表示されない
開発中に出た例外が起きたときも、固定メッセージしか情報が得られません。
デバッグのためにいちいち上記の実装をコメントアウトするのも手間です。
解法: 環境変数を見て例外処理をスキップ
Ruby on Railsは環境変数で開発環境と本番環境を切り替えます。
このルールに乗り、次のように開発中の場合は例外処理をスキップします。
rescue_from StandardError, with -> do
# 開発中は例外をキャッチしません。
raise exception if Rails.env.development?
render plain: 'Something wrong', status: :internal_server_error
end
問題2: テストではレスポンスコードやメッセージを本番と同じにしたい
RSpec
などのテストコードでは本番の振る舞いを確認したいです。
ですので開発中環境のように、例外処理そのものをスキップする実装は望ましくありません。
解法: 例外の内容を標準出力に
例外処理の実装はそのままに、例外の内容を標準出力に書き出します。
本場環境では余計な情報が出力されないように、ここでも環境変数を見て出力するかどうかを切り替えます。
rescue_from StandardError, with ->(exception) do
puts exception.backtrace if Rails.env.test?
render json: { message: 'Something wrong' }, status: :internal_server_error
end
問題3: 例外のバックトレースが長すぎて読みにくい
通常RSpec
を実行する時は、複数のテストケースを実行します。
予期しない例外が起きる時は、複数のテストケースで起きることがあるでしょう。
そのときにバックトレースをすべて表示すると標準出力の表示が流れすぎて見るのが大変です。
解法: ActiveSupport::BacktraceCleanerで出力するバックトレースの情報を減らす
Ruby on RailsにはActiveSupport::BacktraceCleanerというバックトレースの情報を減らすためのクラスが用意されています。
これを使って、gem
(依存ライブラリ)とRSpec
に関するバックトレースの表示をしません。
rescue_from StandardError, with ->(exception) do
if Rails.env.test?
bc = ActiveSupport::BacktraceCleaner.new
bc.add_silencer { |line| line =~ %r{gems|/rspec} }
puts bc.clean exception.backtrace
end
render json: { message: 'Something wrong' }, status: :internal_server_error
end
問題4: 例外の色が黒字だと出力から例外情報をみつけられない
通常RSpec
を実行する時は、複数のテストケースを実行します。
予期しない例外が起きる時は、複数のテストケースで起きることがあるでしょう。
たくさんの標準出力の内容から例外を目grepするのは大変です。
解法: 例外表示の1行目を赤くする
多くのターミナルはANSI escape codeを使うと出力文字に色をつけられます。
これを使って例外名に色を付けます。
またRSPecのエラーと合わせて赤字にすると、情報が必要以上に増えず読みやすくなります。
rescue_from StandardError, with ->(exception) do
if Rails.env.test?
puts "\e[31m", exception.class, "\t#{exception.message}\e[0m"
bc = ActiveSupport::BacktraceCleaner.new
bc.add_silencer { |line| line =~ %r{gems|/rspec} }
puts bc.clean exception.backtrace
end
render json: { message: 'Something wrong' }, status: :internal_server_error
end
問題5: 独自例外が出力される
アプリケーション固有の例外処理を独自例外を使って共通化することがあります。
例外処理の対象をStandardError
にしていると、これらの独自例外も標準出力に表示します。
また、共通の例外処理が上手く実装できているか確認するためのテストコードを書くこともあります。
その場合は、テストを実行すると常に独自例外が標準出力に表示されます。
バックトレースまで表示するので、馬鹿になりません。
偽陽性に慣れると、本当の例外まで見逃すようになります。
解法: 独自例外の時は標準出力に表示しない
例外の出力処理に、キャッチした例外が自作の独自例外か確認するガード条件を追加します。
rescue_from StandardError, with ->(exception) do
if Rails.env.test?
# MyErrorは、意図した例外なので出力しません。
puts_detail_of exception unless exception.is_a? MyError
end
render json: { message: 'Something wrong' }, status: :internal_server_error
end
def puts_detail_of(exception)
bc = ActiveSupport::BacktraceCleaner.new
bc.add_silencer { |line| line =~ %r{gems|/rspec} }
puts "\e[31m", exception.class, "\t#{exception.message}\e[0m"
puts bc.clean exception.backtrace
end
おわりに
良き開発を、例外とともに