patorashのブログ

方向性はまだない

Railsのエラー画面の謎挙動にハマッた

Railsのcontrollerで、トランザクションを使っているところで、rescueブロックの処理が書いてあるのにそこに辿り着けていない現象に遭遇しました。

悩んでいることをチャットワークに書いたら、同僚の @kazuhisa1976 さんが、小さいRailsアプリを作って検証してくれたのですが、そこで面白い挙動が見つかりました。

検証環境を作る

小さいRailsアプリを作る

まず、小さいRailsアプリを作ります。

rails new sample
cd sample
bin/rails g scaffold User name:string
bin/rails db:migrate

Userモデルにvalidationを加えます。

class User < ApplicationRecord
  validates :name, presence: true
end

UsersController#createで、検証のためにトランザクションを使ってみます。保存に失敗したら、root_pathにリダイレクトするという設定です。

class UsersController < ApplicationController
  # 省略
  def create
    @user = User.new(user_params)
    ActiveRecord::Base.transaction do
      @user.save!
    end
    redirect_to @user, notice: 'User was successfully created.'
  rescue => e
    redirect_to root_path
  end
  # 省略
end

Railsアプリを起動しておきます。

bin/rails s

これで、とりあえずの検証環境は出来上がりました。

検証する

正常系

まずは、nameを入力して投稿してみます。

f:id:patorash:20180519084703p:plain

当然ながら成功。

f:id:patorash:20180519084744p:plain

異常系

次は、nameを何も入力せずに投稿してみます。nameが空なので、@user.save!が失敗して、root_pathにリダイレクトされるはずです。

f:id:patorash:20180519084635p:plain

おおっと!リダイレクトされませんでした!rescueブロックに辿りつかずに、@user.save!ActiveRecord::RecordInvalidが発生していると言われました。私が遭遇した現象と全く同じ!

f:id:patorash:20180519084959p:plain

原因を探る

エラー画面のWebコンソールで、実験してみましょう。

f:id:patorash:20180519085705p:plain

はい。実はこのRailsアプリには、root_pathが定義されてません。それが原因です。例外の情報が入っている変数eもあることから、rescueブロックには実は辿りついています。

そうなんです。rescueブロックで発生したエラーの原因を表示せず、元の例外が発生した原因を表示してしまうということです。

修正する

このRailsアプリでいえば、routes.rbでroot_pathを定義してあげれば、直ります。

Rails.application.routes.draw do
  resources :users
  root to: 'users#index' # root_pathを定義
end

まとめ

rescueブロックで例外が発生すると、大元の例外の情報を基にエラー画面が作られてしまうので、rescueブロックに辿り着けないようなエラー画面が表示されたら、rescueブロック内にブレイクポイントを置いて1つ1つ検証していくのがいいです。恐らく、今回と同様に変数かメソッドの定義がないなどが原因です。

自分のエラーの原因も、メソッドがないことが原因でした。 作った当初はRails3か4くらいで、paramsがHashを継承していたのですが、Rails5にアップグレードして(今使ってるのは5.1.4)Hash継承ではなくActionController::Parametersになった影響で、mapメソッドがなくなっていました。

似たようなコードを書くと、以下のような感じ。

# users_paramsはfields_forで作られた配列のデータ
def update
  ActiveRecord::Base.transaction do
    users_params.each do |id, values|
      user = User.find id
      user.assign_attributes values
      user.save!
    end
  end
  redirect_to users_path
rescue => e
  @users = users_params.map do |id, values| # ここでundefined method
    user = User.find id
    user.assign_attributes values
    user.valid?
    user
  end
  render :edit
end

参考情報はこちら。

qiita.com

そこで、以下のように修正。

rescue => e
  @users = users_params.to_h.map do |id, values| # to_hメソッドを挟む
    # 略
  end
  render :edit
end

今回は管理画面側のため、網羅的にテストを書いていなかったことが気づけなかった原因だったので、例外時のテストも追加しておこうと思います。