【问题标题】:How can I access the HTTP response content of a Spring Boot default error response?如何访问 Spring Boot 默认错误响应的 HTTP 响应内容?
【发布时间】:2021-10-08 23:17:38
【问题描述】:

我有一个 Spring Boot 应用程序,它正在捕获对应用程序发出的 REST 请求的 HTTP 响应内容。我这样做是为了记录和将来分析进入系统的请求。

目前,我已将其实现为过滤器 bean(每个 OncePerRequestFilter),使用 ContentCachingResponseWrapper 捕获写入输出流的内容:

@Component
public class ResponseBodyLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        ContentCachingResponseWrapper cachingResponse =
                new ContentCachingResponseWrapper(response);

        try {
            filterChain.doFilter(request, cachingResponse);
        } finally {
            byte[] responseBytes = cachingResponse.getContentInputStream().readAllBytes();
            System.out.println("Response: \"" + new String(responseBytes) + "\"");

            cachingResponse.copyBodyToResponse();
        }
    }
}

这适用于应用程序的大多数请求。但是,它没有捕获的一件事是默认的 Spring Boot 错误响应。它不是捕获响应的内容,而是返回空字符串。

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

控制器:

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/404")
    public void throw404() {
        throw new ResponseStatusException(BAD_REQUEST);
    }
}

HTTP 响应:

{
  "timestamp": "2021-08-03T18:30:18.934+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/test/404"
}

系统输出:

Response: ""

我已经确认,如果我从嵌入式 Tomcat 的 Spring Boot 默认切换到嵌入式 Jetty(使用 spring-boot-starter-jetty 并排除 spring-boot-starter-tomcat),仍然会出现此问题。

如何在我的应用程序中捕获 Spring Boot 错误响应输出?请注意,如果其他解决方案解决了问题,我不需要将其作为过滤器。

【问题讨论】:

  • 您使用的是哪种嵌入式容器?
  • @IndraBasak 我正在使用嵌入式 Tomcat 容器的 Spring Boot 默认值,但我已经确认,如果我使用 spring-boot-starter-jetty 和不包括 spring-boot-starter-tomcat 切换到嵌入式 Jetty 容器,也会发生这种情况。我已经用我的build.gradle 更新了这个问题,并提供了一些额外的细节来明确这一点。
  • 您只是想记录您的回复还是修改您的回复?
  • 留下了答案。
  • @IndraBasak 谢谢。等我有足够的带宽后,我会深入看看你的答案。

标签: java spring spring-boot spring-mvc servlet-filters


【解决方案1】:

我还没有确定一个好的方法来实现在过滤器中获取 Spring Boot 错误响应体的既定目标,但是经过一些调试和深入了解 Spring 内部,我相信我可能已经确定了它的原因至少没用。

看起来BasicErrorController.error(HttpServletRequest request) 是框架中负责返回要呈现的错误对象的部分。

但是,观察调用此控制器方法的位置,在实际过滤发生后调用Servlet.service() 期间似乎正在发生这种情况。根据tomcat-embed-coreApplicationFilterChain

private void internalDoFilter(ServletRequest request,
                              ServletResponse response)
    throws IOException, ServletException {

    // Call the next filter if there is one
    // [...]
    Filter filter = filterConfig.getFilter();
    // [...]
    filter.doFilter(request, response, this);
    // [...]

    // We fell off the end of the chain -- call the servlet instance
    // [...]
    servlet.service(request, response);
    // [...]
}

根据上面的代码,filter.doFilter(request, response, this) 中调用了ResponseBodyLoggingFilter 过滤器,但直到之后servlet.service(request, response) 才调用BasicErrorController.error(...)

【讨论】:

    【解决方案2】:

    我知道您的问题与响应日志记录有关,但是当涉及到错误处理时,请考虑以下方法,它可以在发生错误时补充您的代码。

    正如 Spring Boot 文档中描述 error handling 时所述,可能要走的路是定义一个扩展 ResponseEntityExceptionHandler@ControllerAdvice 并将其应用于您需要的控制器。

    您可以为自定义异常定义ExceptionHandlers 或覆盖ResponseEntityExceptionHandler 中已提供的方法。

    例如,您可以覆盖主要的handleException 方法:

    // will apply for all controllers. The annotation provides attributes for limiting that scope
    @ControllerAdvice
    public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
    
      @Override
      @ExceptionHandler({
        HttpRequestMethodNotSupportedException.class,
        HttpMediaTypeNotSupportedException.class,
        HttpMediaTypeNotAcceptableException.class,
        MissingPathVariableException.class,
        MissingServletRequestParameterException.class,
        ServletRequestBindingException.class,
        ConversionNotSupportedException.class,
        TypeMismatchException.class,
        HttpMessageNotReadableException.class,
        HttpMessageNotWritableException.class,
        MethodArgumentNotValidException.class,
        MissingServletRequestPartException.class,
        BindException.class,
        NoHandlerFoundException.class,
        AsyncRequestTimeoutException.class
      })
      @Nullable
      public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
        // Log errors, obtain the required information,...
        // ...
    
        return super.handleException(ex, request);
      }
    
    }
    

    At the moment,这是该方法提供的默认实现:

    /**
      * Provides handling for standard Spring MVC exceptions.
      * @param ex the target exception
      * @param request the current request
      */
    @ExceptionHandler({
        HttpRequestMethodNotSupportedException.class,
        HttpMediaTypeNotSupportedException.class,
        HttpMediaTypeNotAcceptableException.class,
        MissingPathVariableException.class,
        MissingServletRequestParameterException.class,
        ServletRequestBindingException.class,
        ConversionNotSupportedException.class,
        TypeMismatchException.class,
        HttpMessageNotReadableException.class,
        HttpMessageNotWritableException.class,
        MethodArgumentNotValidException.class,
        MissingServletRequestPartException.class,
        BindException.class,
        NoHandlerFoundException.class,
        AsyncRequestTimeoutException.class
      })
    @Nullable
    public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
      HttpHeaders headers = new HttpHeaders();
    
    
      if (ex instanceof HttpRequestMethodNotSupportedException) {
        HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
        return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
      }
      else if (ex instanceof HttpMediaTypeNotSupportedException) {
        HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
        return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
      }
      else if (ex instanceof HttpMediaTypeNotAcceptableException) {
        HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
        return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
      }
      else if (ex instanceof MissingPathVariableException) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
      }
      else if (ex instanceof MissingServletRequestParameterException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
      }
      else if (ex instanceof ServletRequestBindingException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request);
      }
      else if (ex instanceof ConversionNotSupportedException) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
      }
      else if (ex instanceof TypeMismatchException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
      }
      else if (ex instanceof HttpMessageNotReadableException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
      }
      else if (ex instanceof HttpMessageNotWritableException) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
      }
      else if (ex instanceof MethodArgumentNotValidException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
      }
      else if (ex instanceof MissingServletRequestPartException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request);
      }
      else if (ex instanceof BindException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleBindException((BindException) ex, headers, status, request);
      }
      else if (ex instanceof NoHandlerFoundException) {
        HttpStatus status = HttpStatus.NOT_FOUND;
        return handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request);
      }
      else if (ex instanceof AsyncRequestTimeoutException) {
        HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
        return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, headers, status, request);
      }
      else {
        // Unknown exception, typically a wrapper with a common MVC exception as cause
        // (since @ExceptionHandler type declarations also match first-level causes):
        // We only deal with top-level MVC exceptions here, so let's rethrow the given
        // exception for further processing through the HandlerExceptionResolver chain.
        throw ex;
      }
    }
    

    这个SO question 也很有价值。

    other SO question 提供了可能有趣的不同替代方法,例如,通过扩展DispatcherServlet

    根据您的实际使用情况,也许httptrace actuator 也可以通过启用http tracing 来满足您的要求。

    【讨论】:

      猜你喜欢
      • 2010-10-22
      • 1970-01-01
      • 2017-08-08
      • 2014-12-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-20
      • 2015-08-30
      相关资源
      最近更新 更多