はじめに
本番環境で突然 500 が出て、ローカルでは一切再現できない ――そんな状況に遭遇すると、原因の当たりを付けるだけでも消耗します。
今回私は、 Render×Neon 構成の Rails アプリで、特定ページの表示時に undefined method が発生し、ローカルでは再現できないエラーに直面しました。
アプリは Render の無料枠でデプロイしていたため、 Shellからコマンドが打てないなど制約もあり、さらに大変です。
結論としては、Render のログから「どの種類の例外か」を特定し、対象ページの処理から「エラーになり得る値」を洗い出し、本番データを改めて確認することで、原因の特定をすることができました。
初歩的なエラーでしたが、本番環境でのエラー対処は初めてだったので、後学のために修正までの流れを記事にまとめます。
記事の流れ
- Render×Neon 環境で「本番のみ」エラーが起きた状況
- Render のログから
undefined methodを特定する - 対象ページのメソッドから「nil で落ちそうな箇所」を洗い出す
-
goal.amountに辿り着くが、設計上は nil にならない? - 本番データを再確認 → amount が nil(旧仕様の名残) → 過去の仕様変更が原因と判明
- どうして仕様変更したのか
- バリデーション済みでも「念のため落ちない」よう正規化で修正
- 学び:本番データ、設計変更、ログ調査の型
Render×Neon 環境で本番だけエラーが出た状況
今回のアプリでは、前述の通り Render で Rails をホスティングし、DB は Neon(PostgreSQL)を利用している構成でした。
ある日、いつも通りプルリクエストをした後、本番環境で特定のページを開くと、そこには 500 エラーがあり、画面が表示できなくなっていました。
おかしいなと思い、ローカル環境で同様に操作してみても、再現ができない...
本番環境だけうまくいかない最悪のパターンでした。
ローカル環境で再現できないことから、「コードの単純なバグ」よりも、「本番DBのデータ」または「本番特有の環境」が原因ではないかと考えました。特に Render×Neon のように、ローカルとは別の DB を使っている場合、開発用データでは踏まないケースが本番で顕在化しがちです。
とはいえ、本番環境は特に弄ってないこと、範囲が特定ページだけであることから、「本番DBのデータ」が怪しそうです。
また、エラーが「特定ページだけ」で発生しているということは、
- そのページ固有の集計や表示ロジック
- そのページが参照するデータ(既存レコードの状態)
のどちらかに原因があると考えられます。ここでログだけなら無料枠でも確認できるのではないかと思い、Renderのログを見に行きました。
Render のログで undefined method を特定する
Render のログを確認してみると、「NoMethodError (undefined method zero?' for nil) 」が確認できました。
このログにより、「nil に対して数値メソッドを呼んだ」と原因特定できました。
この時点で、調査対象は一気に狭まります。
次に見るべきは「対象ページで zero? を呼んでいそうな場所」または「0 判定・割合計算をしていそうなメソッド」です。
対象ページのメソッドから nil になり得る値を洗い出す
ログで NoMethodError(nil に対するメソッド呼び出し)だと分かったので、対象ページに関係する計算メソッドを確認しました。
対象ページでは進捗率の計算を行っています。
計算前に「分母が 0 なら 0 を返す」「それ以外は実績/目標で割合を出す」といった形に設計していました。
実際に見つかったのが次のようなメソッドです。
def progress_ratio
return 0 if target_value.zero?
ratio = total_value.to_f / target_value
[ratio, 1.0].min
end
ここで zero? を呼んでいるのは target_value です。つまり、target_value が nil になっていたら nil.zero? で落ちます。
次の一手は単純で、target_value の正体を辿り、 goal.amount という「目標値」を返していることがわかりました。
goal.amount が nil になっていたんですね。
ただし、この時点では「設計上 nil にならないはず」という認識もありました。
なぜなら、設計変更により amount は必ず 1 以上にする前提へ移行しており、バリデーションも追加済みだったからです。
つまり、新規に作成されるデータでは amount が nil にならないはずです。
にもかかわらず本番で落ちている。
次は本番データを確認しに行きました。
本番データを再確認すると amount が nil だった(旧仕様の名残)
本番環境を改めて確認したところ、amount が nil のレコードが存在していました。
過去の設計では、「チェックするだけ」の目標であれば amount(量)の概念は不要だと考え、nil を許容していました。しかしその後、目標を回数型に変更した際、このままでは過去のログが意味を持たなくなり、データとして破綻してしまうことに気づき、設計を変更しました。
この設計変更は公開前に行っており、当時はユーザーも存在しなかったため、既存の DB データには手を加えませんでした。
結果として、その判断が本番環境で残り続けていた旧仕様のデータと現在の実装とを噛み合わせ、今回のエラーとして表面化することになりました。
amount を正規化して「万が一 nil でも落ちない」ようにする
原因が goal.amount の nil だと確定したため、修正は大きく2つの方向性が考えられます。
- 「DB を移行して既存の nil を埋める(データ修正)」、
- 「アプリ側で nil を許容し、計算ロジックを落ちないようにする(防御的実装)」
今回は、バリデーションにより今後の新規データでは発生しないこと、そして既存データは開発者しか持っていないことから無視して良いと考え、後者を採用しました。
具体的には、target_value を返す箇所で数値へ正規化し、nil の場合は 0 として扱うようにしました。
def target_value
goal.amount.to_i
end
これにより、target_value.zero? の判定が必ず安全に行えます。
また、分母が0のときは 計算せずに0を返す設計なので、nil を 0 に正規化しても意味が破綻しません。
重要なのは「どこで正規化するか」です。view 側で || 0 をしても、メソッド内部で例外が起きた時点で view に戻らないため、防げません。例外が起き得る“手前”で正規化する必要があります。
修正後は本番環境で該当ページを開き、500 が出ないことを確認しました。加えて、Neon/Render のログでも同様の例外が出ていないことを確認してクローズしました。
バリデーションがあっても「落ちない実装」は価値がある
「これからのユーザーには起きない」ことが分かっていても、本番には過去データが残ります。また、将来の仕様変更や想定外の入力経路で nil が入り得る余地はゼロにはできません。
計算系・集計系の処理は、落ちるより 0 を返すほうがユーザー体験としても安全なケースが多いと学びました。
まとめ
本記事では、Render×Neon 構成の Rails アプリにおいて、本番環境でのみ発生した undefined method エラーについて、調査から修正までの流れを振り返りました。
設計自体は、過去のログを壊さないために行ったものであり、判断として誤っていたわけではありませんでした。ただし、過去の状態が本番データとして残っている以上、「今の設計では起こらないはず」という前提だけでは不十分であることを学びました。
今回は、既存データの移行は行わず、amount を数値として正規化することで、万が一 nil が混入しても落ちない実装に修正しました。本番環境でのエラー対応では、ログから事実を拾い、コードとデータの両方を確認し、設計の履歴まで含めて原因を整理することが重要だと感じています。
同じように「本番でだけ再現するエラー」に遭遇した際の一つの調査例として、参考になれば幸いです。