13.

Spring Boot「Whitelabel Error Page」原因と対処|独自エラーページの実装方法

編集
この記事の要点
  • Spring Boot の Whitelabel Error Page
  • 原因: 独自のエラーページ (/error マッピング)が定義されていないためのフォールバック表示
  • 対処1: 独自エラーコントローラ @Controller + /error を実装
  • 対処2: application.propertiesserver.error.whitelabel.enabled=false
  • 対処3: src/main/resources/templates/error.html を置く(Thymeleaf 自動マッピング)

エラー画面

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 はデフォルトのフォールバックエラーページとして「Whitelabel Error Page」を表示します。これは:

  • 独自の /error コントローラが定義されていない
  • テンプレートエンジンの error.html も無い
  • 結果として Spring Boot 内蔵の素っ気ない HTML が表示される

本番では UX もブランディングも崩れるので独自エラーページに置き換えます。

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

もっとも簡単。src/main/resources/templates/error.html を作成するだけで Spring Boot が自動でマッピング:




    
    エラー
    


    

500

Internal Server Error


        
トップに戻る

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

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

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

対処2: ErrorController を実装

テンプレートを使わず、コントローラで JSON 等を返す:

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 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 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

例外型ごとに細かく分岐したい場合(推奨):

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

@ControllerAdvice
public class GlobalExceptionHandler {

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

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

    @ExceptionHandler(Exception.class)
    public ResponseEntity 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 を無効化

独自のエラーハンドリングだけ使い、Spring の Whitelabel を完全に切る:

# 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 開発でのエラー詳細制御

# 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 専用ハンドラ
@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 を自動切替

同じ /error でも Accept ヘッダで HTML / JSON を切り替えるパターン:

@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 json(HttpServletRequest request) {
        // JSON で返す(API リクエスト用)
        // ...
        return Map.of("status", status, "error", "...");
    }
}

関連

  • HandlerInterceptor でエラー処理前にロギング・通知を挟める
  • Spring Security の AccessDeniedHandler: 403 の独自処理
  • RestControllerAdvice: API 専用の @ControllerAdvice
  • ProblemDetail (RFC 9457): Spring 6 / Boot 3 で標準化されたエラーレスポンス形式
  • Actuator: 健全性チェック / エラー統計の収集
編集
Post Share
子ページ

子ページはありません

同階層のページ
  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 ... hat could not be found.
  12. Annotation-specified bean name ' ... ' for bean class [ ... ] conflicts with existing, non-compatible bean definition of same name and class [...]
  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