◀ 13.

Whitelabel Error Page の原因と消し方|Spring Boot のエラーページ

▶
この記事の要点
  • Spring Boot の Servlet / MVC 自動構成での Whitelabel Error Page は、エラー応答を HTML 表示する既定のフォールバック。Boot は通常 /error を既に提供しており、この文言だけで元の障害原因は分からない
  • 原因の切分け: 元URLの HTTP status・URL/method・ログを確認。404 の経路不一致、500 の例外、認証・認可の拒否は別問題
  • 表示の変更1: 独自エラーコントローラ @Controller + /error は必要な場合だけ実装し、元の HTTP status を保持する
  • 表示の変更2: Boot 2.x / 3.x の server.error.whitelabel.enabled=false は既定画面を無効にするだけで、404 / 500 の原因は直さない
  • 表示の変更3: Thymeleaf 等のテンプレート解決が構成済みなら src/main/resources/templates/error.html を使える(通常URLの追加マッピングではない)

エラー画面

Spring Boot アプリにアクセスして 404 / 500 等のエラーが発生したとき、以下のような無味乾燥なページが表示されます:

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sat May 15 12:34:56 JST 2024
There was an unexpected error (type=Not Found, status=404).

原因

ここでは Spring Boot の Servlet / Spring MVC 自動構成を扱います。Spring Framework 単体や WebFlux の設定と同一視しません。Boot の既定の BasicErrorController が通常 /error を処理し、HTML の解決先がない場合に Whitelabel が使われます。「no explicit mapping」は元URLの controller が必ず存在しない、という診断ではありません。これは:

  • 独自の /error コントローラが定義されていない
  • 適用できる独自の error view や status 別エラーページが解決されていない
  • 結果として Spring Boot 内蔵の素っ気ない HTML が表示される

まず元URL・HTTP method・context path / servlet path と実際の HTTP status を確認します。404 は URL マッピングと controller の登録・component scan 範囲(通常は @SpringBootApplication のパッケージ配下)、静的資源の配置を分けて確認します。500 は対応する時刻の例外ログで原因を絞ります。401 / 403 やログインへのリダイレクトは認証・認可も確認し、アクセス拒否を 404 と決め付けません。画面の置換や Whitelabel の無効化は障害原因の修正とは別です。ログ全文・トークン・接続文字列等を公開しないでください。

対処1: error.html を配置(Thymeleaf)

Thymeleaf とその view resolver が構成済みの場合は src/main/resources/templates/error.html を error view として解決できます。テンプレートを置くだけで Thymeleaf が導入されたり、元URLの処理が追加されたりはしません。次の旧テンプレートは trace / message を表示するため、公開環境では詳細を渡さず、スタックトレース・内部例外文を外へ出さないでください。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${status} + ' - ' + ${error}">エラー</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div class="error-container">
        <h1 th:text="${status}">500</h1>
        <h2 th:text="${error}">Internal Server Error</h2>
        <p th:if="${message}" th:text="${message}"></p>

        <div th:if="${trace}" class="trace">
            <pre th:text="${trace}"></pre>
        </div>

        <a href="/">トップに戻る</a>
    </div>
</body>
</html>

ステータスコードごとに別ファイルを置くと自動で振り分けられます:

src/main/resources/templates/error/
├── 404.html         ← 404 専用
├── 500.html         ← 500 専用
└── ...

src/main/resources/templates/
└── error.html       ← その他すべて

対処2: ErrorController を実装

次は旧コードの抜粋です。javax.servlet は Boot 2.x 世代で、Boot 3.x は jakarta.servlet を使います。Map / HashMap / Date の import も省略されています。Boot 2.x内でも世代差があり、2.3/2.4等は非推奨でもgetErrorPath()の実装が必要です。掲載クラスはそのままでは未実装エラーになり、2.5で同メソッドは削除されています。非推奨化と実装不要を混同せず、server.error.pathの設定とも区別してください。JSON 内の status フィールドは HTTP status を設定しません。実装時は ERROR_STATUS_CODE を検証し、ResponseEntity.status(...) 等で応答にも反映する必要があります。通常リクエストとして /error を直接開くと error 属性が無い場合もあり、ERROR dispatch と区別します。ERROR_MESSAGE や元URIをそのまま公開せず、安全な応答項目を選んでください。Boot 4 では ErrorController は org.springframework.boot.webmvc.error.ErrorController に移っています。

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;

@RestController
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public Map<String, Object> handleError(HttpServletRequest request) {
        Integer status = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        String message = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
        String path = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);

        Map<String, Object> response = new HashMap<>();
        response.put("status", status);
        response.put("error", message);
        response.put("path", path);
        response.put("timestamp", new Date());

        return response;
    }
}

対処3: @ControllerAdvice で例外を catch

次は MVC の例外処理の抜粋で、NotFoundException / log はアプリ側定義、ResponseEntity / HttpStatus / AccessDeniedException 等の import は別途必要です。record は Java 16 以降の構文です。Spring Security の filter chain で発生する認証・認可例外を ControllerAdvice がすべて捕捉するわけではなく、AuthenticationEntryPoint / AccessDeniedHandler の役割と区別します。また汎用 Exception ハンドラで本来 404 等の例外を 500 に変えないよう対象と優先順位を確認し、ex.getMessage() の機密情報を応答へ流さないでください。

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleForbidden(AccessDeniedException ex) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ErrorResponse("FORBIDDEN", ex.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
        log.error("Unexpected error", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL", "サーバエラーが発生しました"));
    }

    public record ErrorResponse(String code, String message) {}
}

対処4: Whitelabel を無効化

Boot 2.x / 3.x で server.error.whitelabel.enabled=false にすると既定の Whitelabel view を無効化しますが、独自処理だけになる保証はなく、Servlet コンテナのエラー画面へ戻る場合があります。次の旧 properties はそのままコピーしないでください。値の後の # は行末コメントとして除外されないため、注釈を独立したコメント行へ移します。Boot 3.5 の属性出力の選択肢は never / always / on_param で、on_trace_param はその選択肢にありません。on_param は要求側が詳細出力を求められる条件であり、公開環境の安全策として選びません。Boot 4 の該当キーは spring.web.error.* なので server.error.* を全版共通と扱いません。パス変更時は固定の /error マッピングとの不一致にも注意します。

# application.properties
server.error.whitelabel.enabled=false

# エラー詳細表示の制御
server.error.include-stacktrace=on_param    # never / always / on_param / on_trace_param
server.error.include-message=on_param        # never / always / on_param
server.error.include-binding-errors=on_param
server.error.include-exception=false

# /error パスを変更
server.error.path=/api-error

本番 vs 開発でのエラー詳細制御

次の旧例でも値の後の # 注釈は別行へ移します。例えば # 機密漏洩防止 を独立した行にし、設定行は server.error.include-stacktrace=never だけにします。Boot 2.x / 3.x の例で、使用中の profile と有効設定を確認し、開発用 always を公開環境へ適用しません。Boot 3.5 の include-binding-errors も公開環境では never とし、独自 controller / advice が返す詳細はこれらのプロパティだけでは抑止できません。

# application-dev.properties (開発)
server.error.include-stacktrace=always
server.error.include-message=always
server.error.include-exception=true

# application-prod.properties (本番)
server.error.include-stacktrace=never        # 機密漏洩防止
server.error.include-message=never
server.error.include-exception=false

404 だけを別ハンドラに

次はハンドラ未発見の旧例で、すべての 404 を捕捉する例ではありません。Spring Framework 6.1 以降は NoHandlerFoundException を投げる動作が既定となり、旧 throw-exception-if-no-handler-found 設定を一律に追加する必要はありません。Boot 3.5 の既定の静的資源マッピング /** に一致して資源が無い場合は NoResourceFoundException となり、別の例外です。add-mappings=false は静的資源配信自体を無効化するので、404 対策として無条件に適用しません。MVC へ届く前の拒否や別 servlet の 404 もこの Advice の対象と同じではありません。

// 404 専用ハンドラ
@ControllerAdvice
public class NotFoundHandler {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ModelAndView handle404(NoHandlerFoundException ex) {
        ModelAndView mav = new ModelAndView("error/404");
        mav.setStatus(HttpStatus.NOT_FOUND);
        return mav;
    }
}

// application.properties で 404 例外を投げるように
// spring.mvc.throw-exception-if-no-handler-found=true
// spring.web.resources.add-mappings=false

JSON / HTML を自動切替

この同名ErrorControllerにも上記のBoot 2.x内のgetErrorPath()実装条件が及びます。またMap.ofはJava 9以降で、下記のstatusのスコープを補うだけではJava 8対応にはなりません。

既定の BasicErrorController にも HTML とその他の応答の分岐があります。次の旧例はその考え方の未完成な抜粋で、JSON メソッドの status は HTML メソッドのローカル変数であり参照できず、このままではコンパイルできません。JSON 側でも status の取得・欠落時処理と HTTP 応答への反映が必要です。独自 ErrorController は既定処理を置き換えるため、既存クラスと同名の2例を同時導入せず、エラー時の status / Content-Type と直接アクセス時を分けて確認します。

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping(value = "/error", produces = MediaType.TEXT_HTML_VALUE)
    public String html(HttpServletRequest request, Model model) {
        // HTML テンプレートで返す
        Integer status = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        model.addAttribute("status", status);
        return "error";
    }

    @RequestMapping(value = "/error")
    @ResponseBody
    public Map<String, Object> json(HttpServletRequest request) {
        // JSON で返す(API リクエスト用)
        // ...
        return Map.of("status", status, "error", "...");
    }
}

関連

  • HandlerInterceptor: MVC ハンドラ処理の範囲で利用する。Security filter の拒否や全 ERROR dispatch を必ず通る仕組みではない
  • Spring Security の AccessDeniedHandler: 認可拒否の処理。未認証時の AuthenticationEntryPoint と区別する。元の REQUEST とエラー表示用の ERROR dispatch は別に認可され得るため、適用中の filter chain と dispatcher 条件を確認し、認証解除や全URL公開で表示だけ直さない
  • RestControllerAdvice: API 専用の @ControllerAdvice
  • ProblemDetail (RFC 9457): Spring 6 / Boot 3 で標準化されたエラーレスポンス形式
  • Actuator: 健全性チェック / エラー統計の収集

公式仕様の確認先:Spring Boot3.5 Servletのエラー処理、Boot4.0プロパティ、Java Propertiesの構文(2026-10-28確認)。画面・設定・コードは対象版と各例の前提条件を照合してください。

子ページ

子ページはありません

同階層のページ
  1. java.lang.IllegalStateException: CGLIB is required to process @Configuration classes
  2. Error creating bean with name 'org.springframework.aop.config.internalAutoProxyCreator': Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: Could not initialize class org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
  3. No mapping found for HTTP request with URI ... in DispatcherServlet with name ...
  4. An internal error occurred during: "Building UI model". com/google/common/base/Function
  5. No identifier specified for entity : ...
  6. org.hibernate.hql.internal.ast.QuerySyntaxException: table_name is not mapped
  7. No compiler is provided in this environment
  8. java.sql.SQLException: The server time zone value ' ... ' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone
  9. Caused by: java.lang.RuntimeException: Executing an update/delete query
  10. Not supported for DML operations
  11. Field ... required a bean of type ... that could not be found.
  12. Annotation-specified bean name 'xxxController' for bean class [com.xxx.controller.xxxController] conflicts with existing, non-compatible bean definition of same name and class [com.xxx.xxxController]
  13. Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback.
  14. Exception in thread "main" java.lang.UnsupportedClassVersionError

最近更新/作成されたページ