7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

その404本当に「Not Found」ですか?

Last updated at Posted at 2025-12-06

はじめに

Railsには便利な機能がたくさんありますが、便利すぎて逆に困ることもあります。今回はその一つ、ActiveRecord系例外の自動ステータスコード変換について書いてみます。

Railsの挙動

Railsでは、特定の例外が発生すると自動的にHTTPステータスコードが決まります。

  • ActiveRecord::RecordNotFound → 404 Not Found
  • ActiveRecord::RecordInvalid → 422 Unprocessable Entity
  • ActiveRecord::StaleObjectError → 409 Conflict

User.find(params[:id])で見つからなければ自動で404。便利ですよね。

何が問題なのか

でも、ちょっと待ってください。

RecordNotFoundが発生するのは、本当に「リソースが存在しない」ときだけでしょうか?

例1: マスタデータの参照

class OrdersController < ApplicationController
  def show
    @order = Order.find(params[:id])
    @prefecture = Prefecture.find(@order.prefecture_id)  # マスタ整理で消えてたら?
  end
end

こんな極端な例は普通ないと思いますが、似たようなケースに遭遇したことありませんか?

例2: タスクの通知設定

class Tasks::NotificationsController < ApplicationController
  def update
    @task = Task.find(params[:id])
    @notification_setting = @task.notification_settings.find_by!(user: Current.user)
    @notification_setting.update!(enabled: params[:enabled])
  end
end

通知設定画面に来ている時点で、設定レコードは存在するはず。でも過去のバグやバッチ処理の不具合で消えていたら...?

例3: タスク複製時のバリデーションエラー

class Tasks::DuplicatesController < ApplicationController
  def create
    @original = Task.find(params[:task_id])
    @new_task = Task.new(
      title: @original.title,
      description: @original.description,
      project_id: @original.project_id
    )
    @new_task.save!
  end
end

元タスクの project_id をそのままコピーしている。でも、titleのバリデーションが元タスクの作成時より厳しくなっていたら...?


これらは本来500(サーバーエラー)であるべきなのに、404や422が返ってしまいます。

何がまずいかというと:

  • エラー監視で見落とされる(かもしれない) 404は正常系として扱われがち
  • 原因調査が遅れる 「ユーザーが存在しないIDを叩いただけでしょ」と思われる
  • データ不整合が放置される

仕組みを理解する

実はこの挙動、ActionDispatch::ExceptionWrapperというミドルウェアが担っています。

# Rails内部の定義(一部抜粋)
# https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@rescue_responses = {
  "ActiveRecord::RecordNotFound"   => :not_found,
  "ActiveRecord::RecordInvalid"    => :unprocessable_entity,
  "ActiveRecord::StaleObjectError" => :conflict,
  # ... 他にも色々
}

そして嬉しいことに(?)、この設定は上書き可能です。

カスタマイズしてみる

そこで、ActiveRecord系の例外は自動変換しない設定にしましょう。

# config/initializers/exception_handling.rb

ActionDispatch::ExceptionWrapper.rescue_responses =
  Hash.new(:internal_server_error).merge!(
    {
      "ActionController::RoutingError" => :not_found,
      "ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
      # ActiveRecord系は意図的に除外
      # "ActiveRecord::RecordNotFound" => :not_found,
      # "ActiveRecord::RecordInvalid"  => :unprocessable_entity,
    }
  )

ポイント:

  • Hash.new(:internal_server_error)で、未定義の例外は全て500に
  • RoutingErrorなど、本当に「リソースがない」ケースだけ404に
  • ActiveRecord系は各コントローラで明示的にハンドリング

コントローラでの明示的なハンドリング

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    # ここは本当に「ユーザーが存在しない」ケース
    render_not_found
  end
end

「面倒じゃない?」と思うかもしれますが、むしろ例外が発生しうる箇所を意識できるというメリットがあります。

ハンドリングを忘れた? 大丈夫、500エラーとしてエラー監視に引っかかります。気づけます。

まとめ

方針 メリット デメリット
Rails標準 楽、コードが少ない データ不整合、不具合を見逃すリスク
明示的ハンドリング 異常系を確実に検知 面倒, Rails Wayから外れる

Railsの「便利なデフォルト」は多くのケースで正しく動きます。でも、「便利」と「安全」はトレードオフになることもあるということは覚えておいて損はないかなと思います。

特にデータ整合性が重要なアプリケーションでは、今回のような設定を検討してみてはいかがでしょうか。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?