【问题标题】:How to print complete error message when logging an exception generated by RestTemplate?记录 RestTemplate 生成的异常时如何打印完整的错误消息?
【发布时间】:2021-10-07 05:10:31
【问题描述】:

我们有一个基于 Spring-Boot (2.3.10.RELEASE) 的应用程序,它使用 RestTemplate 调用许多 REST API。

如果任何 REST API 返回任何 4xx 或 5xx HTTP 错误代码以及消息正文,则不会记录完整的消息正文。

这是一个最小的可重现示例:

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

@Slf4j
public class RestTemplateTest {

    @Test
    void shouldPrintErrorForRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        try {
            restTemplate.getForEntity("http://hellosmilep.free.beeceptor.com/error/notfound", String.class);
        } catch (Exception e) {
            log.error("Error calling REST API", e);
        }
    }
}

输出:

10:28:11.347 [main] ERROR com.smilep.java.webapp.RestTemplateTest - Error calling REST API
org.springframework.web.client.HttpClientErrorException$NotFound: 404 Not Found: [{
  "glossary": {
    "title": "example glossary",
    "GlossDiv": {
      "title": "S",
      "GlossList": {
        "GlossEntry": {
          "ID": "SGML",
          "SortAs": "SGML",
          "Glo... (593 bytes)]
    at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:113)
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:184)
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:125)
    at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:780)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:738)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:672)
    at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:340)
    at com.smilep.java.webapp.RestTemplateTest.shouldPrintErrorForRestTemplate(RestTemplateTest.java:15)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:212)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.util.ArrayList.forEach(ArrayList.java:1257)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at java.util.ArrayList.forEach(ArrayList.java:1257)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

您可以注意到响应消息在上面的堆栈跟踪中被"Glo... (593 bytes)] 截断。如何记录完整的错误响应正文?

我知道我可以执行以下操作来打印错误响应正文,但是在许多类(以及其他应用程序)中添加的样板代码太多。

if(e instanceof HttpClientErrorException) {
    log.error("response body : " + ((HttpClientErrorException) e).getResponseBodyAsString());
}
  1. 有什么方法可以记录Exception 对象将记录完整的错误响应消息正文而不是用... (593 bytes)] 截断?

  2. java 中是否有一些默认大小限制,之后它会截断错误日志中的任何消息?如果是,如何更改?

更新:

在 jccampanero 回答之后,我向 Spring 团队提出了 this 问题,以使此日志消息大小限制可配置。他们完全取消了限制。此更改应该在 Spring 的某些未来版本中可用。

【问题讨论】:

    标签: java spring spring-boot error-handling resttemplate


    【解决方案1】:

    By defaultRestTemplate 使用 DefaultErrorHandler 处理错误。

    在这个类的实现中,不同的handleError 方法——例如this——依赖于getErrorMessage method

    /**
      * Return error message with details from the response body, possibly truncated:
      * <pre>
      * 404 Not Found: [{'id': 123, 'message': 'my very long... (500 bytes)]
      * </pre>
      */
    private String getErrorMessage(
        int rawStatusCode, String statusText, @Nullable byte[] responseBody, @Nullable Charset charset) {
    
    
      String preface = rawStatusCode + " " + statusText + ": ";
      if (ObjectUtils.isEmpty(responseBody)) {
        return preface + "[no body]";
      }
    
    
      if (charset == null) {
        charset = StandardCharsets.UTF_8;
      }
      int maxChars = 200;
    
    
      if (responseBody.length < maxChars * 2) {
        return preface + "[" + new String(responseBody, charset) + "]";
      }
    
    
      try {
        Reader reader = new InputStreamReader(new ByteArrayInputStream(responseBody), charset);
        CharBuffer buffer = CharBuffer.allocate(maxChars);
        reader.read(buffer);
        reader.close();
        buffer.flip();
        return preface + "[" + buffer.toString() + "... (" + responseBody.length + " bytes)]";
      }
      catch (IOException ex) {
        // should never happen
        throw new IllegalStateException(ex);
      }
    }
    

    如您所见,这是实际截断消息的方法。

    为了提供完整的消息,你可以自己提供ResponseErrorHandler实现。

    例如重用DefaultErrorHandler中的代码实现:

    import java.io.IOException;
    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.lang.Nullable;
    import org.springframework.util.ObjectUtils;
    import org.springframework.web.client.DefaultResponseErrorHandler;
    import org.springframework.web.client.HttpClientErrorException;
    import org.springframework.web.client.HttpServerErrorException;
    import org.springframework.web.client.UnknownHttpStatusCodeException;
    
    public class CustomRestTemplateResponseErrorHandler extends DefaultResponseErrorHandler {
      
      // This overloaded method version is only available since Spring 5.0
      // For previous versions of the library you can override
      // handleError(ClientHttpResponse response) instead
      @Override
      protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
        String statusText = response.getStatusText();
        HttpHeaders headers = response.getHeaders();
        byte[] body = getResponseBody(response);
        Charset charset = getCharset(response);
        String message = getErrorMessage(statusCode.value(), statusText, body, charset);
    
        switch (statusCode.series()) {
          case CLIENT_ERROR:
            throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
          case SERVER_ERROR:
            throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
          default:
            throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
        }
      }
    
      /**
       * Return error message with details from the response body:
       * <pre>
       * 404 Not Found: [{'id': 123, 'message': 'actual mesage']
       * </pre>
       *
       * In contrast to <code>DefaultResponseErrorHandler</code>, the message will not be truncated.
       */
      private String getErrorMessage(
          int rawStatusCode, String statusText, @Nullable byte[] responseBody, @Nullable Charset charset) {
    
        String preface = rawStatusCode + " " + statusText + ": ";
        if (ObjectUtils.isEmpty(responseBody)) {
          return preface + "[no body]";
        }
    
        if (charset == null) {
          charset = StandardCharsets.UTF_8;
        }
    
        // return the message without truncation
        return preface + "[" + new String(responseBody, charset) + "]";
      }
    }
    

    然后,配置RestTemplate 以使用此自定义ResponseErrorHandler

    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.web.client.HttpClientErrorException;
    import org.springframework.web.client.RestTemplate;
    
    @Slf4j
    public class RestTemplateTest {
    
        @Test
        void shouldPrintErrorForRestTemplate() {
            RestTemplate restTemplate = new RestTemplate();
            ResponseErrorHandler errorHandler = new CustomRestTemplateResponseErrorHandler();
            restTemplate.setErrorHandler(errorHandler);
    
            try {
                restTemplate.getForEntity("http://hellosmilep.free.beeceptor.com/error/notfound", String.class);
            } catch (Exception e) {
                log.error("Error calling REST API", e);
            }
        }
    }
    

    请参阅this related SO questionthis article 了解更多示例。

    【讨论】:

    • 感谢您的详细解答。我已经向 Spring 团队提出了 this 问题,以便在 DefaultResponseErrorHandler 中配置 maxChars
    • 不客气@Smile。太好了,干得好!我绝对同意你的观点,我也考虑过提出类似问题的可能性。我希望您能尽快得到反馈,并且该问题将在框架的未来版本中得到解决。
    • Spring 已移除此消息大小限制。我已将此更新添加到我的问题中。谢谢!
    • 这太棒了@Smile!我很高兴听到这个消息!
    【解决方案2】:

    如果你能捕捉到 HttpClientErrorException,而不是做一个,这样你就可以避免样板

    @Test
    void shouldPrintErrorForRestTemplate() {
        try {
    
            RestTemplate restTemplate = new RestTemplate();
            restTemplate.getForEntity("http://hellosmilep.free.beeceptor.com/error/notfound", String.class);
    
        } catch(HttpClientErrorException e) {
            log.error(e.getResponseBodyAsString(),e);
        }catch (Exception e) {
            log.error("Error calling REST API", e);
        }
    }
    

    【讨论】:

    • 是的,我可以这样做,但我更倾向于知道为什么错误消息被截断,截断消息是否有一些大小限制,如果是 - 如何更改此限制。跨度>
    猜你喜欢
    • 1970-01-01
    • 2023-02-22
    • 2022-10-14
    • 1970-01-01
    • 1970-01-01
    • 2012-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多