AIによる要約
Spring Bootの500 Internal Server Errorは、リクエスト処理中にサーバー側で未処理の例外が発生したときに返される代表的なステータスです。画面のWhitelabel Error Pageだけでは原因を特定できないため、発生時刻、URL、入力条件を記録し、サーバーログの例外型と最後のCaused by、自分たちのコードの行番号を確認します。NullPointerException、DB制約違反、SQL、テンプレート、JSON変換、外部APIなど原因は複数あります。catch Exceptionで隠したり例外メッセージを利用者へそのまま返したりせず、入力エラー、業務エラー、システムエラーを分けて適切なステータスへ変換します。


Spring Bootの開発中、画面に500 Internal Server ErrorやWhitelabel Error Pageが表示されることがあります。500は「Spring Bootが壊れた」という意味ではなく、サーバーがリクエストを処理する途中で、正常なレスポンスを返せない問題が起きたことを表します。
原因はJavaの例外、DB、テンプレート、JSON変換、外部APIなど幅広いため、当てずっぽうでコードを修正してはいけません。現象を再現できる情報を残し、ログを上からではなく構造として読み、最初に直すべき原因へ到達します。
この記事のポイント
- 500はサーバー処理中の未処理例外が代表的な原因
- 発生時刻、URL、入力、利用者操作を先に記録する
- ログの例外型、Caused by、自分たちのコード行を読む
- NullPointerExceptionだけに決めつけない
- catch Exceptionやreturn nullで原因を隠さない
- 入力・業務・システムエラーを分けてレスポンスへ変換する
500 Internal Server Errorの意味
HTTP 500は、サーバー側で処理を完了できない予期しない状態が起きたときに使われる汎用的なステータスです。Spring Bootは標準で/errorへのエラー処理を用意しており、ブラウザとAPIクライアントで表示形式が異なる場合があります。
ただし、画面に表示された500は結果であって原因ではありません。次のコードでは、利用者が見つからない状態を考慮せずnullを使ったため、処理中に例外が発生します。
@GetMapping("/users/{id}")
public UserResponse find(@PathVariable Long id) {
User user = userService.find(id); // 見つからないとnull
return new UserResponse(
user.getId(),
user.getName()); // NullPointerException
}この場合、根本原因は「500」ではなく、未存在をnullで表し、その後の処理で考慮していないことです。未存在を404、業務エラー、空の結果のどれにするか決めて修正します。
500は調査開始の印です。修正対象は、ログに残った元の例外と、その例外を起こした設計です。
最初に発生条件を記録する
ログを探す前に、どのリクエストを追うか決めます。複数人が同じ環境を使っていると、同時刻に別の例外が出ていることがあります。
- 発生日時とタイムゾーン
- 画面名またはAPIのURL
- HTTPメソッド
- 入力した値
- ログイン利用者や業務ID
- 毎回発生するか、特定データだけか
- 直前にデプロイ・設定変更があったか
個人情報や認証情報をチケットへ貼らないよう注意しつつ、注文IDや処理IDなど調査に必要な識別子を残します。再現手順がない「500になりました」だけでは、別担当者が同じ現象へたどり着けません。
ログはCaused byと自分たちのコード行を見る
Springの例外は複数層に包まれるため、先頭の例外名だけでは原因が分からないことがあります。ログを下へたどり、最後に近いCaused byと、自分たちのパッケージ名が出る最初の行を確認します。
org.springframework.web.util.NestedServletException:
Request processing failed
at org.springframework.web.servlet...
Caused by: java.lang.NullPointerException:
Cannot invoke "User.getName()" because "user" is null
at com.example.user.UserService.createResponse(UserService.java:48)
at com.example.user.UserController.find(UserController.java:31)この例なら、まずUserService.java:48を開きます。Spring内部の長いスタックをすべて読むのではなく、例外型、メッセージ、自分たちのコード行、呼び出し順を拾います。
Spring Bootで500になる代表的な原因
| 原因 | ログの例 | 確認場所 |
|---|---|---|
| nullの考慮漏れ | NullPointerException | Service、変換処理、戻り値 |
| DB制約違反 | DataIntegrityViolationException | 一意制約、NOT NULL、外部キー |
| SQL・接続失敗 | SQLException、JDBC例外 | SQL、接続先、権限 |
| テンプレート処理 | TemplateInputExceptionなど | View名、式、Model属性 |
| JSON変換 | HttpMessageConversionExceptionなど | 循環参照、getter、型 |
| 外部API | タイムアウト、接続例外 | URL、認証、応答、リトライ |
同じ500でも修正方法は異なります。まず例外型を特定し、その例外が起きた業務データとコード行を確認します。
DB制約違反を入力チェックだけで直さない
一意制約や外部キー違反が500になった場合、画面の入力チェックを追加するだけでは競合を完全には防げません。DB制約は最後の整合性保証として残し、想定できる違反を業務エラーへ変換します。
@Transactional
public void register(UserForm form) {
if (userRepository.existsByEmail(form.email())) {
throw new DuplicateEmailException(form.email());
}
userRepository.save(
new User(form.name(), form.email()));
}同時登録では事前確認の直後に別トランザクションが登録する可能性があります。DB制約違反も適切に処理し、どこまで利用者へ伝えるかを設計します。例外メッセージにSQLやテーブル構造をそのまま出してはいけません。
catch Exceptionで500を隠さない
画面を止めないために、Controllerで全例外を捕まえて正常レスポンスを返す修正は危険です。
// NG: 障害を成功扱いし、原因も失う
@PostMapping("/orders")
public String register(OrderForm form) {
try {
orderService.register(form);
return "redirect:/orders";
} catch (Exception e) {
return "order/form";
}
}このコードでは、入力ミス、DB停止、プログラム不具合がすべて同じ画面へ戻ります。ログもなければ運用監視から異常を検知できません。
// 改善例: 予測できる業務例外を共通変換する
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicate(
DuplicateEmailException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse(
"EMAIL_ALREADY_REGISTERED",
"このメールアドレスは登録済みです"));
}
}すべての例外を無理に変換する必要はありません。想定する入力・業務エラーを明示し、想定外の例外は適切に記録して500として監視対象にします。
ログへメッセージだけを残さない
// NG: スタックトレースが残らない
log.error("注文登録に失敗: {}", e.getMessage());
// 例外オブジェクトも渡す
log.error("注文登録に失敗 orderId={}", orderId, e);障害調査に必要なのは、エラーメッセージだけでなく例外型、スタックトレース、対象を特定できる識別子です。一方で、パスワード、トークン、カード情報などをログへ出さないようにします。
画面へ例外メッセージをそのまま出さない
e.getMessage()をAPIレスポンスや画面へそのまま返すと、SQL、クラス名、ファイルパスなど内部情報が漏れる可能性があります。利用者向けメッセージと、開発・運用向けログを分けます。
注意
本番で一時的に詳細エラーを表示する設定へ変える前に、情報漏えいと設定の戻し忘れを考慮してください。原則として、詳細は権限管理されたログや監視基盤で確認します。
テストで異常系を再現する
正常系だけでは、未存在や業務例外が500へ漏れることに気づけません。ControllerAdviceを含めたテストで、期待するステータスとエラーコードを確認します。
@Test
void 登録済みメールなら409を返す() throws Exception {
willThrow(new DuplicateEmailException("a@example.com"))
.given(userService)
.register(any(UserRequest.class));
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name":"Tanaka","email":"a@example.com"}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code")
.value("EMAIL_ALREADY_REGISTERED"));
}想定外の例外まで握りつぶして200になるテストになっていないかも確認します。エラー応答の形式は、既存APIの共通仕様へ合わせます。
現場で使える500エラーの確認順
- 発生時刻、URL、入力、対象IDを記録する
- HTTPステータスが本当に500か確認する
- 同時刻のサーバーログを探す
- 最後のCaused byと例外メッセージを読む
- 自分たちのコードの最初の行番号を開く
- 同じ入力で再現するか確認する
- 入力・業務・システムエラーのどれか判断する
- 修正後に正常系と異常系のテストを追加する
障害連絡で伝える内容
14:32に注文登録API
POST /api/ordersで500を確認しました。注文ID12345で再現し、ログの根本原因はDataIntegrityViolationExceptionです。配送先IDがマスタに存在しないデータで外部キー違反になっています。影響範囲を確認し、入力時の業務チェックと例外変換を追加します。
「500が出ています」だけでなく、いつ、どこで、どの条件で、どの例外が、どのデータに対して発生したかを分けて伝えると、調査と判断が進みます。
提出前のセルフチェック
レビュー前に確認すること
- 発生条件を再現できる形で残したか
- 最後のCaused byまで読んだか
- 自分たちのコード行を確認したか
- 入力エラーを500にしていないか
- 業務エラーのステータスを決めたか
- catch Exceptionで握りつぶしていないか
- 例外オブジェクトをログへ渡しているか
- 機密情報をログへ出していないか
- 内部例外を画面へそのまま返していないか
- 異常系テストを追加したか
Spring Bootの例外処理を学ぶ参考書
500エラーを正しく扱うには、ControllerだけでなくService、トランザクション、例外ハンドラ、ログの役割をつなげて理解する必要があります。
作って学ぶ Spring Boot入門
Spring Bootを、動くWebアプリを作りながら覚える。
Controller、画面、DB連携などを一つのアプリとして実装できます。Java基礎からSpringの現場へ進む橋渡しに向く一冊です。
- Spring Bootを手を動かして学びたい
- Webアプリ開発の流れをつかみたい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
Spring Bootの500 Internal Server Errorは結果であり、原因はログに残る例外です。発生時刻、URL、入力、対象IDを記録し、最後のCaused byと自分たちのコード行から調査します。
入力エラー、業務エラー、想定外のシステムエラーを分け、適切なHTTPステータスと利用者向けメッセージへ変換してください。catch Exceptionで隠さず、スタックトレースと識別子を安全に記録し、異常系テストまで追加することが再発防止になります。
Spring Bootの標準エラー処理はSpring Boot公式ドキュメントでも確認できます。
