JavaのServletで404・405・500エラーが出たら?原因と確認する場所

最終更新日

Schoo Java入門 中級 第5回 サーバーサイドアプリケーションの実装基礎

Servletが動かないときは、いきなりコード全体を書き直すのではなく、まず「どのURLへ、GETとPOSTのどちらで送って、何番の応答が返ったか」を確認します。404ならURLと配置、405なら送信方法と受け取り方、500ならサーバーログが、調査の出発点です。

たとえば「フォームからは動くのに、URLを直接開くと405になる」「数量に文字を入れると500になる」は、別々の原因です。この記事では、同じ注文金額の確認プログラムを使って、エラーを再現する操作、見る場所、修正、直ったかの確認までつなげます。

Schoo「Java入門 中級」第5回「サーバーサイドアプリケーションの実装基礎」の補足記事です。Java 17・Tomcat 10.1の jakarta.servlet を前提にしています。

404・405・500では、最初に確認する場所が違う

番号意味の目安最初に見るもの
404 Not Found要求したリソースが見つからない実際に送ったURL、アプリの配置場所、ServletのURL設定
405 Method Not AllowedそのURLで、そのHTTPメソッドを受け付けていないGET/POSTと、doGet/doPostの対応
500 Internal Server Errorサーバー側で処理を完了できない問題が起きた同じ操作・時刻に対応するサーバーログ

番号はHTTPステータスコードです。原因を一つに決めつけるものではありません。404が必ずつづり間違いとは限らず、500が必ず数値変換の失敗とも限りません。以下では代表的な原因を実際に再現します。なお、「接続できません」のようにHTTP応答自体を受け取れない場合は、まずTomcatの起動状態・ホスト・ポートを確認します。

イラスト:エラー番号から確認する場所を分ける

404の例はorder-preveiwという誤ったURLとorder-previewの設定を比較する。405の例はGETとdoPostの不一致を確認する。500の例はサーバーログのNumberFormatExceptionとOrderPreviewServlet.javaの17行目を確認する。
本文で再現する3つの例です。404と500の矢印は確認する情報の対応を示し、HTTPの通信経路ではありません。画像を選択すると拡大できます。

まずブラウザで「失敗したリクエスト」を特定する

  1. ブラウザの開発者ツールで Network(ネットワーク) を開く。
  2. フォーム送信など、問題が起きた操作をもう一度行う。
  3. 送信先のリクエストを選び、Headersの Request URL・Request Method・Status Code を確認する。
  4. POSTの入力内容はPayloadのForm Data、GETの入力内容はURLのクエリパラメータも確認する。

赤い行が複数あっても、最初に見るのは今回の操作の送信先です。たとえばfavicon.icoの404だけを見て、ServletのURLを直してはいけません。リダイレクトがある場合はログを保持し、最初のPOSTと、そのあとのGETを分けて確認します。

ブラウザのConsoleと、Tomcatのコンソールは別です。Javaの例外を探す場所は、Eclipseなどから起動したTomcatのコンソール、またはTomcatのサーバーログです。Networkは「何を送って何が返ったか」、サーバーログは「サーバー内で何が起きたか」を調べます。

再現に使うプログラム:1個500円の商品を計算する

数量3なら合計1500円を表示するだけのプログラムです。注文登録やDB更新はしません。画面遷移やJSPの問題を混ぜないため、結果はServletから短いテキストで返します。実際のアプリの画面構成は、別記事で扱ったServlet・Model・JSPの役割分担を使えます。

アプリのコンテキストパスを /lesson05 とします。自分の環境で違う名前なら、その部分だけ読み替えてください。次のServletは500を再現するため、入力チェックを省いたコードです。後半に修正版を掲載します。

入力画面:src/main/webapp/order-form.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>注文金額の確認</title>
</head>
<body>
    <h1>注文金額の確認</h1>
    <p>1個500円の商品です。注文の登録は行いません。</p>
    <form action="order-preview" method="post">
        <label for="quantity">数量(1〜99)</label>
        <input id="quantity" name="quantity" type="text"
               inputmode="numeric">
        <button type="submit">金額を確認</button>
    </form>
</body>
</html>

今回は文字入力で起きる問題も調べるため、type=”text”にしています。inputmode=”numeric”は数字を入力しやすいキーボードの指定であり、サーバー側の入力チェックの代わりにはなりません。

エラー再現用:src/main/java/example/OrderPreviewServlet.java

package example;

import java.io.IOException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/order-preview")
public class OrderPreviewServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws IOException {
        request.setCharacterEncoding("UTF-8");
        String text = request.getParameter("quantity");
        int quantity = Integer.parseInt(text);
        int total = quantity * 500;
        response.setContentType("text/plain; charset=UTF-8");
        response.getWriter().println("合計: " + total + "円");
    }
}

http://localhost:8080/lesson05/order-form.html を開き、まず3を入力します。「合計: 1500円」と表示され、Networkが POST /lesson05/order-preview → 200 なら基準の動作を確認できています。ここから変更する箇所は一度に一つにします。

404の例:フォームの送信先URLを間違えた

フォームのactionだけを、次のように変更します。preview の末尾を preveiw と取り違えています。

<form action="order-preveiw" method="post">

3を入力して送信すると、このサンプルでは404になります。確認するのは「Javaのクラス名とURLが同じか」ではなく、実際のURLと、コンテキストパス・@WebServletの組み合わせです。

確認する情報今回の値
NetworkのRequest URLhttp://localhost:8080/lesson05/order-preveiw
アプリのコンテキストパス/lesson05
Servletの設定@WebServlet("/order-preview")
正しい送信先/lesson05/order-preview

フォームをアプリ直下のorder-form.htmlから開いているので、相対URLのaction=”order-preview”は /lesson05/order-preview になります。actionをorder-previewへ戻し、同じフォームからPOST送信して200になることを確認してください。OrderPreviewServletというクラス名を、そのままURLの末尾にするわけではありません。

つづりが正しくても404なら、次の順で切り分けます。

  • 入力画面order-form.htmlも404なら、コンテキストパスとアプリのデプロイ・起動結果を確認する。Tomcat自体が動いていても、そのアプリの起動に失敗している場合がある。
  • 入力画面は200でServletだけ404なら、@WebServletのパス、最新クラスの反映、アプリ起動時のエラーを確認する。コンパイルエラーが残っていないかも見る。
  • Servletでの処理後に404なら、forward先のJSPなどが本当に存在するか、パスや大文字・小文字が合っているか確認する。

forward先のJSPが存在しない場合も404になることがあります。404だからServletの処理が一切動いていない、とまでは断定できません。ブレークポイントやサーバーログで、どこまで到達したかも確認します。

405の例:doPostしかないServletへGETを送った

actionを正しいorder-previewへ戻したうえで、フォームから method="post" を外して送信します。methodを省略したフォームはGETになるため、このサンプルでは405になります。ServletのURLをアドレスバーに直接入力する場合や、通常のリンクをクリックする場合もGETです。

<form action="order-preview">
確認する情報今回の状態
Request URL/lesson05/order-preview?quantity=3
Request MethodGET
Servletに書いた処理doPostだけ
結果GETを処理するdoGetがないため405

修正は、今回決めた送信方法に合わせてフォームのmethod=”post”を戻すことです。URLを直接開くのではなく、入力画面から送信し直します。POSTで200になれば、Servletの登録先とPOST処理は動いていることが分かります。

検索画面など、もともとGETで受け付ける設計ならdoGetを実装するのが適切です。しかし、405を消す目的だけでdoGetからdoPostを呼ぶのは避けます。登録や削除までGETで実行できてしまう可能性があるためです。どちらを使うかは、処理の役割に合わせて決めます。

リダイレクト後だけ405になる場合も、移動先で実際にGETが送られていないかを確認します。POST専用のURLへリダイレクトするのではなく、表示用のGETを受け付けるURLへ進める構成を検討します。

500の例:数量にabcを入れて数値へ変換した

フォームのactionとmethodを元に戻し、数量に abc と入力して送信します。送信先は正しくPOSTにも対応していますが、エラー再現用コードの Integer.parseInt(text) でNumberFormatExceptionが起き、この例では500になります。

ブラウザに詳細な例外が表示されるかは、サーバーの設定によって異なります。500という画面だけでも、サーバー側のログを確認できます。Eclipseなら実行中のTomcatのコンソールを選びます。単独起動のTomcatなら、実行環境の CATALINA_BASE/logs 以下など、設定された出力先を確認します。ファイル名や出力先は起動方法によって変わります。

サーバーログの読み取り例(抜粋)

java.lang.NumberFormatException: For input string: "abc"
    at java.base/java.lang.Integer.parseInt(...)
    at example.OrderPreviewServlet.doPost(OrderPreviewServlet.java:17)
    ...

これは今回の例外から、読む箇所を抜き出した表示です。日時・Tomcat側の行やJDK内部の行番号は省略しています。17行目は、この記事のエラー再現用Javaファイルをそのまま使った場合の位置です。空行やimportを追加すれば行番号も変わります。

ログの箇所読み取ること次の確認
NumberFormatException数値への変換に失敗したどの値を変換しようとしたか
For input string: “abc”abcを変換しようとしたフォームの入力とPayloadのquantity
OrderPreviewServlet.java:17自分のファイルの17行目で例外が表に出ているInteger.parseInt(text)の直前でtextを確認する

最初に見つかったTomcat内部のファイルを直すわけではありません。例外の種類とメッセージを読み、自分のパッケージexampleの行へ進みます。Caused by: がある場合は、包まれた例外の原因もたどり、その部分にある自分のコードの行を確認します。

ブラウザのPayloadではquantity=abcになっているのか、そもそもquantityが送られていないのかも区別します。後者なら、HTMLのnameとgetParameterの名前が一致しているかを確認します。画面にある入力欄でも、nameが違えば期待した名前では取得できません。

修正版:入力の問題を500のままにしない

利用者がabcを入力する可能性はあります。そこで、数量が存在するか、整数にできるか、1〜99の範囲かを確認してから計算します。同じファイルの内容を次のコードへ置き換えます。再現用と修正版を同時に配置するのではありません。

package example;

import java.io.IOException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/order-preview")
public class OrderPreviewServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws IOException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/plain; charset=UTF-8");
        String text = request.getParameter("quantity");
        if (text == null || text.isBlank()) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().println("数量を入力してください。");
            return;
        }

        int quantity;
        try {
            quantity = Integer.parseInt(text.trim());
        } catch (NumberFormatException e) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().println("数量は1〜99の整数で入力してください。");
            return;
        }

        if (quantity < 1 || quantity > 99) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().println("数量は1〜99の整数で入力してください。");
            return;
        }

        int total = quantity * 500;
        response.getWriter().println("合計: " + total + "円");
    }
}

ここでは入力不正に対し、400 Bad Requestと固定メッセージを返す設計にしています。setStatus()だけではメソッドは終わらないため、returnで後続の計算を止めます。tryの範囲は数値変換だけにし、予想したNumberFormatExceptionを扱っています。

400はこのサンプルで選んだ入力エラーの応答です。入力JSPへエラーを渡して再表示する設計もあります。大切なのは、想定できる入力不正を、原因不明の500として放置しないことです。一方、何でも catch (Exception e) で囲んで200を返すと、本当の不具合まで成功に見えてしまいます。

HTML側にrequiredやtype=”number”を追加しても、サーバーへ別の方法でリクエストは送れます。サーバー側の検証は残してください。この例は入力値をそのままHTMLへ埋め込まず、固定メッセージと検証後の数値だけをテキストで返しています。

修正後の確認表:正常系だけで終わらせない

操作エラー再現用修正版
正しいURLへPOST、数量3200・合計1500円200・合計1500円
正しいURLへPOST、数量abc500400・整数の入力を案内
正しいURLへPOST、数量が空500400・数量の入力を案内
正しいURLへPOST、数量0/100200・範囲外も計算してしまう400・1〜99を案内
正しいURLへGET405405(POST専用の設計は同じ)
誤ったorder-preveiwへPOST404404(URLの誤りは別途直す)

入力チェックを追加しても、間違ったURLやHTTPメソッドは直りません。「どの変更で、どの失敗が直るのか」を分けて確認します。逆に、200でも数量0を受け付けていた再現用コードのように、業務上の要件を満たしていない場合があります。番号だけでなく表示結果も確認してください。

500の原因が数値変換ではなかった場合

ログの手がかり確認する内容
NullPointerExceptionnullのまま使った変数は何か。パラメータやrequest属性の名前、取得できない経路を確認する。
JSPのコンパイルエラー・JasperExceptionログに示されたJSPの行、import、Javaの構文や型を確認する。JSPもサーバー側で処理される。
SQLExceptionなどDB関連の例外原因メッセージ、接続先、SQL、接続障害を確認する。入力変換とは切り分ける。

これらも未処理のまま伝われば500の原因になり得ますが、アプリ側の例外処理や応答状態によって表示は変わります。「500だからこの修正」と決めず、同じ操作のログに出た事実から調べます。修正後はビルド・デプロイが成功していることを確認し、同じ入力で再試験します。

調査結果を残すときの書き方

操作: order-form.htmlで数量abcを入力し送信
リクエスト: POST /lesson05/order-preview
結果: HTTP 500
例外: NumberFormatException
位置: OrderPreviewServlet.java:17
原因: 数値でない文字列を検証せずparseIntへ渡していた
修正: 未入力・変換失敗・範囲外を分けて検証
再確認: 数量abcは400、数量3は200・合計1500円

「動きません」だけでなく、この情報があると自分でも他の人でも追いやすくなります。ただし、ログや画面を共有するときはパスワード・Cookie・セッションID・個人情報などを除いてください。公開環境では、内部の例外詳細やファイル構成を利用者へそのまま表示せず、サーバー側で調査できるようにします。

確認問題

アプリは /lesson05、Servletの設定は @WebServlet(“/order-preview”)、doPostのみとします。修正版の入力仕様は数量1〜99です。

  1. フォームが POST /lesson05/OrderPreviewServlet を送って404になりました。どの情報を比較し、送信先をどう直しますか。
  2. フォームからPOSTすると200ですが、/lesson05/order-preview?quantity=3 をアドレスバーから開くと405です。原因と、このサンプルでの確認方法を答えてください。
  3. HTMLが name="count"、Javaが getParameter("quantity") になっています。値3を入れたのに、エラー再現用コードではNumberFormatExceptionによる500でした。何を確認し、どこを修正しますか。
  4. 修正版で、数量abcと数量3をPOSTしたとき、それぞれ何番とどの表示を期待しますか。GETで405が残ることは、この修正の失敗でしょうか。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • 失敗したリクエストのURL・HTTPメソッド・ステータスをNetworkで確認する。
  • 404はURLと配置、405はGET/POSTの対応、500はサーバーログから調査を始める。
  • 例外の種類・原因メッセージ・自分のコードの行を対応させる。
  • 原因を一つずつ修正し、同じ入力と正常な入力の両方で確認する。
  • エラー番号だけで原因や修正成功を断定しない。

関連記事

シェアする