如何在 Spring Boot 3.2 中覆盖默认 Tomcat 404 not find html 页面?

问题描述 投票:0回答:1

我正在开发 Spring Boot 3.2 (Apache Tomcat 10) 应用程序,并设置了自定义上下文路径 (

/api
)。我想用 JSON/仅状态响应替换 Tomcat 的默认 HTML 错误响应。但是,我在处理上下文路径之外(例如,在裸基 URL 上)发生的 404 错误时遇到了困难。我明白了:

...
<body>
    <h1>HTTP Status 404 – Not Found</h1>
    <hr class="line" />
    <p><b>Type</b> Status Report</p>
    <p><b>Description</b> The origin server did not find a current representation for the target resource or is not
        willing to disclose that one exists.</p>
    <hr class="line" />
    <h3>Apache Tomcat/10.1.19</h3>
</body>
...

我的配置:

全局异常处理程序

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGlobalException(Exception ex) {
        log.error("An error occurred: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ApiResponse(false, ex.getMessage()));
    }

    @ExceptionHandler(NoResourceFoundException.class)
    public ResponseEntity<?> handleNoResourceFoundException(NoResourceFoundException ignored) {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
}

安全配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.csrf(AbstractHttpConfigurer::disable)
            .cors(Customizer.withDefaults())
            .exceptionHandling(exception ->
                    exception.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.NOT_FOUND)))
            .authorizeHttpRequests(auth -> auth.requestMatchers(PERMIT_ALL_URL_PATTERNS.toArray(new String[0]))
                    .permitAll()
                    .anyRequest()
                    .authenticated())
            .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
            .formLogin(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .oauth2Login(loginConfigurer -> loginConfigurer
                    .userInfoEndpoint(endpointConfig -> endpointConfig.userService(customOAuth2UserService))
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    .authorizationEndpoint(authEndPoint ->
                            authEndPoint.authorizationRequestRepository(authorizationRequestRepository))
                    .failureHandler(oAuth2AuthenticationFailureHandler))
            .build();
}

观察到的问题:

  • 对于上下文路径中的 URL,一切都会按预期处理。
  • 对于上下文路径之外的 URL,将返回 Tomcat 的默认 HTML 错误页面。

我尝试过的:

  • spring.mvc.throw-exception-if-no-handler-found
    - 已弃用,新的 Spring Boot 版本中不再抛出 IIUC NoHandlerFoundException。
  • error.whitelabel.enabled=false
    - 对我来说没用,因为我的应用程序中可能发生的所有错误都由全局异常处理程序处理。

自定义错误控制器

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public ResponseEntity<Map<String, String>> handleError(HttpServletRequest request) {
        Object status = request.getAttribute("javax.servlet.error.status_code");
        HttpStatus httpStatus = HttpStatus.valueOf(Integer.parseInt(status.toString()));

        Map<String, String> response = new HashMap<>();
        response.put("error", httpStatus.getReasonPhrase());
        response.put("message", "The requested URL was not found on this server.");

        return new ResponseEntity<>(response, httpStatus);
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

自定义错误报告阀

@Slf4j
public class CustomTomcatErrorValve extends ErrorReportValve {

    @Override
    protected void report(Request request, Response response, Throwable throwable) {

        if (!response.setErrorReported()) return;

        if (log.isDebugEnabled())
            log.debug(
                    "Tomcat failed to prepare the request for spring (set response code to {}).",
                    response.getStatus(),
                    throwable);

        HttpStatus status = HttpStatus.valueOf(response.getStatus());

        try {

            response.setContentType("application/problem+json");
            Writer writer = response.getReporter();
            writer.write(String.format(
                    """
                    {
                        "title": "%s",
                        "status": %d
                    }""",
                    status.getReasonPhrase(), status.value()));
            response.finishResponse();
        } catch (IOException ignored) {
        }
    }
}

异常处理过滤器:

@Slf4j
@Component
public class UnhandledExceptionHandlerFilter extends OncePerRequestFilter {

    private static class StatusCodeCaptureWrapper extends HttpServletResponseWrapper {

        @Getter
        private Integer statusCode;

        @Getter
        private final HttpServletRequest request;

        @Getter
        private final HttpServletResponse response;

        public StatusCodeCaptureWrapper(HttpServletRequest request, HttpServletResponse response) {
            super(response);
            this.request = request;
            this.response = response;
        }
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException {
        StatusCodeCaptureWrapper responseWrapper = new StatusCodeCaptureWrapper(request, response);
        Throwable exception = null;

        try {
            chain.doFilter(request, responseWrapper);
        } catch (ServletException e) {
            exception = e.getRootCause();
        } catch (Throwable e) {
            exception = e;
        }

        if (exception != null
                && !"ClientAbortException".equals(exception.getClass().getSimpleName())) {
            ensureErrorStatusCodeSet(responseWrapper);
            response.setStatus(responseWrapper.getStatusCode());
            handleException(request, response, responseWrapper.getStatusCode(), exception);
        }

        response.flushBuffer();
    }

    private void ensureErrorStatusCodeSet(StatusCodeCaptureWrapper responseWrapper) {
        if (responseWrapper.getStatusCode() == null) {
            responseWrapper.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private void handleException(
            HttpServletRequest request, HttpServletResponse response, int statusCode, Throwable throwable)
            throws IOException {
        log.error("Sending error response status {} for {} because of", statusCode, request.getRequestURI(), throwable);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        Map<String, String> responseBody = new HashMap<>();
        responseBody.put("error", HttpStatus.valueOf(statusCode).getReasonPhrase());
        responseBody.put("message", throwable.getMessage());
        response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
    }
}

如何配置 Spring Boot 3.2 始终返回 JSON 错误响应,尤其是 404 错误,而不是 Tomcat 的默认 HTML 错误页面,即使对于上下文路径之外的 URL 也是如此?

任何指导或解决方案将不胜感激!

java spring-boot tomcat error-handling
1个回答
0
投票

此配置对我有用 - 我们需要两个类,CustomTomcatErrorValve 和 TomcatConfig

自定义TomcatErrorValve.java

@Slf4j
public class CustomTomcatErrorValve extends ErrorReportValve {

    @Override
    protected void report(Request request, Response response, Throwable throwable) {
        if (!response.setErrorReported()) {
            return;
        }

        int statusCode = response.getStatus();
        String reasonPhrase = HttpStatus.valueOf(statusCode).getReasonPhrase();

            try {
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            String jsonResponse = String.format("""
                {
                    "title": "%s",
                    "status": %d
                }
                    """, reasonPhrase, statusCode);
            response.getWriter().write(jsonResponse);
            response.getWriter().flush();

            log.error("Error reported: {} {} - URI: {}", statusCode, reasonPhrase, request.getRequestURI(), throwable);
        } catch (IOException e) {
            log.error("Failed to write custom error response", e);
        }
    }
}

TomcatConfig.java

@Configuration
public class TomcatConfig {

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> factory.addContextCustomizers(context -> {
            try {
                StandardHost host = (StandardHost) context.getParent();
            host.setErrorReportValveClass(CustomTomcatErrorValve.class.getName());
            } catch (Exception e) {
                throw new RuntimeException("Failed to set custom ErrorReportValve", e);
            }
        });
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.