1

I'm using Spring Boot 3 (3.3.1) and Java 21. When using the new RestClient I encountered two problems:

  1. the onStatus(...) method does not work if error 408 (timeoutexception) occurs. And as a result, I cannot handle this exception in onStatus.

  2. How can I override response when processing error statuses in the onStatus(...) method so that the further .body(...) method can process the result? (For example, returning a null object rather than throwing an org.springframework.web.client.ResourceAccessException error)

As an example, I created a small test case on GitHub (here is a link to it): Server: -Here I simply created a controller with one endpoint, which simply waits 5 seconds and returns the string "Hello World":

    @GetMapping("/exception")
    public String exception() throws InterruptedException {
        log.info("let's wait a little");
        Thread.sleep(5000);
        log.info("We waited. Now let's return the result");

        return "Hello World";
    }

Client: I defined a RestClient bean in which I configured the connect=2 sec and read=1 sec timeouts:

    @Bean
    public RestClient restClient(RestClient.Builder builder) {
        return builder
                .baseUrl("http://localhost:8082")
                .requestFactory(requestFactory())
                .build();
    }

    private ClientHttpRequestFactory requestFactory() {
        ClientHttpRequestFactorySettings requestFactorySettings = ClientHttpRequestFactorySettings.DEFAULTS
                .withConnectTimeout(Duration.ofSeconds(2))
                .withReadTimeout(Duration.ofSeconds(1));

        return new BufferingClientHttpRequestFactory(ClientHttpRequestFactories.get(requestFactorySettings));
    }

Then from a simple controller I call the service method, in which I make a request to the server:

    @GetMapping("/check")
    public String check() {
        return simpleService.sayHelloFromServer();
    }

Service:

public class SimpleService {
    private final RestClient restClient;

    public String sayHelloFromServer() {
        return restClient.get()
                .uri(uriBuilder -> uriBuilder.path("/exception").build())
                .retrieve()
                .onStatus(HttpStatusCode::isError, (request, response) -> log.error("handle error [{}]", response.getStatusCode()))
                .body(String.class);
    }
}

But as I see in the logs, the onStatus(...) method does not work. Because the retrieve() method throws an exception.

This can be seen in the logs:

2024-07-22T11:51:06.217+02:00 DEBUG 12612 --- [client] [nio-8081-exec-3] o.s.web.servlet.DispatcherServlet        : Failed to complete request: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8082/exception": Read timed out
2024-07-22T11:51:06.218+02:00 ERROR 12612 --- [client] [nio-8081-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8082/exception": Read timed out] with root cause

java.net.SocketTimeoutException: Read timed out
    at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:278) ~[na:na]
    at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:304) ~[na:na]
    at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346) ~[na:na]
    at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796) ~[na:na]
    at java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099) ~[na:na]
    at java.base/java.io.BufferedInputStream.fill(BufferedInputStream.java:291) ~[na:na]
    at java.base/java.io.BufferedInputStream.read1(BufferedInputStream.java:347) ~[na:na]
    at java.base/java.io.BufferedInputStream.implRead(BufferedInputStream.java:420) ~[na:na]
    at java.base/java.io.BufferedInputStream.read(BufferedInputStream.java:399) ~[na:na]
    at java.base/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:827) ~[na:na]
    at java.base/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:759) ~[na:na]
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1690) ~[na:na]
    at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1599) ~[na:na]
    at java.base/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:531) ~[na:na]
    at org.springframework.http.client.SimpleClientHttpRequest.executeInternal(SimpleClientHttpRequest.java:88) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.http.client.AbstractStreamingClientHttpRequest.executeInternal(AbstractStreamingClientHttpRequest.java:70) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.http.client.BufferingClientHttpRequestWrapper.executeInternal(BufferingClientHttpRequestWrapper.java:77) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal(DefaultRestClient.java:492) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.retrieve(DefaultRestClient.java:460) ~[spring-web-6.1.11.jar:6.1.11]
    at by.gvu.client.service.SimpleService.sayHelloFromServer(SimpleService.java:18) ~[main/:na]
    at by.gvu.client.controller.SimpleController.check(SimpleController.java:15) ~[main/:na]
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.11.jar:6.1.11]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.11.jar:6.1.11]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.11.jar:6.1.11]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.11.jar:6.1.11]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.11.jar:6.1.11]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:389) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.26.jar:10.1.26]
    at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

2024-07-22T11:51:06.234+02:00 DEBUG 12612 --- [client] [nio-8081-exec-3] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}

4
  • The problem is your configuration. You get a read timeout as Nothing is being returned, if nothing is being returned there is also no status code. If you check the retrieve methods internals you can see this. It isn't a lazy stream which is only executed upon body is being called, all steps here are directly invoked. So the retrieve will throw an exception even before the onStatus could do anything. Split the calls and put a try/catch in there. Or create a functional wrapper for this. Commented Jul 22, 2024 at 11:44
  • Sorry, but I didn't quite understand your answer. I allocated to a separate local variable RestClient.ResponseSpec responseSpec = restClient.get().uri(uriBuilder -> uriBuilder.path("/exception").build()).retrieve(); But I still get exception on .retrieve(). As I understand it, this function does not return any lazy streams. (I'm currently using BufferingClientHttpRequestFactory, but I also tried ClientHttpRequestFactories.get(requestFactorySettings) - there was no change in behavior.) Commented Jul 22, 2024 at 12:38
  • And I also don’t understand how to create a wrapper. Since the DefaultRestClient class is final: final class DefaultRestClient implements RestClient. To create a wrapper, I need to copy the entire class =( Commented Jul 22, 2024 at 12:38
  • 1
    No you don't need to extend the class you need a function that accepts a RestClient.ResponseSpec and returns it, which hides the details of the exception handling, so that you still can have a fluent api. The error occurs on the retrieve() call so the onStatus won't even be reached, as there is no response (there is nothing exception an exception). Or just wrap everything in a try/catch and return a default String upon an exception. Commented Jul 22, 2024 at 12:49

2 Answers 2

2

The easiest fix wrap everything in a try/catch.

public String sayHelloFromServer() {
  try {
    return restClient.get()
            .uri(uriBuilder -> uriBuilder.path("/exception").build())
            .retrieve()
            .onStatus(HttpStatusCode::isError, (request, response) -> log.error("handle error [{}]", response.getStatusCode()))
            .body(String.class);
  } catch (ResourceAccessException rae) {
    return rae.getMessage();
  }
}

Which will return a message when a ResourceAccessException occurs.

Or you could wrap the call to retrieve() in an error handling function which would return a default ClientResponse and give it http status code 408 (or whatever you want to do). Which would require you to implement the ResponseSpec yourself (which is a bit cumbersome I noticed).

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the answer. But I was just trying to avoid try/catch blocks. I thought that since I have the onStatus tool, then status processing should be in this place. As a result, I don’t use a ready-made solution (onStatus) and do error handling in try/catch, or break the logic into two blocks, onStatus and try/catch.
Can you help with the second question? Is it possible in the onStatus method, when receiving an error (for example 500 or 503, when we have a response and status), to rewrite the response to an empty response?
That is what I stated in the last comment in my answer. You would need to implement the ResponseSpec. There is no response, so no status. There is no way for it to reach the onStatus as it fails before that.
You might be able to write a ClientInterceptor with a try catch, and return an empty ClientResponse so the chain doesn't break. That is the easiest I suspect.
Spring Boot ClientInterceptor: youtube.com/watch?v=nedhXAU8U4s
-1

After edit:

onStatus() wont handle SocketTimeoutException, beacuse the exception is thrown in your client, and not your server. onStatus() will be invoked when a Response with Error comes back. But when your client timed out, then your client throws the exception

So you need to handle it with try_catch

2 Comments

There is no mistake in this. My configuration simply simulates a slow service. And my client should be able to correctly handle TimeoutExceptions
He wants to be able to handle the timeout situation which is why it is smaller then the wait.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.