🐈

Spring BootでTomcatデフォルトのエラーページをカスタマイズする

に公開

環境

  • JDK 21
  • Spring Boot 3.2.4
  • Embedded Tomcat 10.1.19

やりたいこと

Spring Bootにもエラーページの機能があります。具体的にはsrc/main/resources/templates直下にerror.html、src/main/resources/templates/error直下に404.html・500.htmlなどを作成すればOKです(公式ドキュメント)。

しかし、サーブレットフィルターのレベルで例外がスローされると、前述のエラーページではなくTomcatデフォルトのエラーページが表示されてしまいます。

スクリーンショット 2024-04-11 16.11.02.png

これを自作のエラーページに差し替えたいです。

自作エラーページの作成

静的なHTMLを作成して、適当な場所・適当なファイル名で配置します。今回はsrc/main/resources/templates/tomcatフォルダにtomcat-error.htmlという名前で配置することにします。

ErrorReportValveのカスタマイズ

Tomcatデフォルトのエラーページをレスポンスしているのが、TomcatのErrorReportValveクラスです。これを継承+オーバーライドすることで、作成した自作エラーページをレスポンスするよう変更します。

CustomErrorReportValve.java
package com.example;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.coyote.ActionCode;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Tomcatエラーページをカスタマイズします。
 */
public class CustomErrorReportValve extends ErrorReportValve {

    /**
     * カスタマイズしたエラーページをレスポンスします。
     * @see ErrorReportValve#report(Request, Response, Throwable)
     * @param request   The request being processed
     * @param response  The response being generated
     * @param throwable The exception that occurred (which possibly wraps a root cause exception
     */
    @Override
    protected void report(Request request, Response response, Throwable throwable) {
        int statusCode = response.getStatus();

        // Do nothing on a 1xx, 2xx and 3xx status
        // Do nothing if anything has been written already
        // Do nothing if the response hasn't been explicitly marked as in error
        // and that error has not been reported.
        if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
            return;
        }

        // If an error has occurred that prevents further I/O, don't waste time
        // producing an error report that will never be read
        AtomicBoolean result = new AtomicBoolean(false);
        response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
        if (!result.get()) {
            return;
        }
        // ここまでのコード👆は、ErrorReportValve#report()からコピーしました

        // カスタマイズしたエラーページをレスポンス
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        try (OutputStream os = response.getOutputStream();
             InputStream is = this.getClass().getResourceAsStream("/templates/tomcat/tomcat-error.html")) {
            IOTools.flow(is, os);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

TomcatがCustomErrorReportValveを使うように設定

ServerConfig.java
package com.example;

import org.apache.catalina.Container;
import org.apache.catalina.core.StandardHost;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 組み込みサーバーに関する設定です。
 */
@Configuration
public class ServerConfig {

    /**
     * Tomcatのエラー画面を自作のものに変更します。
     */
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> webServerFactoryCustomizer() {
        return factory -> factory.addContextCustomizers(context -> {
            Container parent = context.getParent();
            if (parent instanceof StandardHost host) {
                host.setErrorReportValveClass(CustomErrorReportValve.class.getName());
            }
        });
    }
}

参考URL

Discussion