HelloKoding

Practical coding guides

Spring Boot RestTemplate Error Handling

RestTemplate throws RestClientResponseException subtypes such as HttpClientErrorException, HttpServerErrorException and UnknownHttpStatusCodeException separately if the response HTTP status code is 4xx, 5xx and unknown

You can handle RestTemplate errors at the local level by catching the RestClientResponseException, at the bean level by implementing the ResponseErrorHandler interface and plugging into a RestTemplate bean

Let’s walk through this tutorial to explore them in more detail examples

The RestClientResponseException

RestClientResponseException is a common base class for exceptions that contain actual HTTP response data

You can use getRawStatusCode, getStatusText, getResponseHeaders, getResponseBodyAsString to get HTTP status code in integer number, get HTTP response headers, and get HTTP response body as a String. They are useful when you need to log and return detail error to the client

Catch RestClientResponseException

RestTemplate can only return a ResponseEntity with HTTP 2xx status. With 4xx and 5xx status, RestTemplate throws HttpClientErrorException and HttpServerErrorException which are extended from HttpStatusCodeException and RestClientResponseException hierarchically

<T> ResponseEntity consumeWebService(String url, Class<T> responseType) {
    try {
        return restTemplate.getForEntity(url, responseType);
    } catch (RestClientResponseException e) {
        return ResponseEntity
            .status(e.getRawStatusCode())
            .body(e.getResponseBodyAsString());
    }
}

Beside catching RestClientResponseException, you can also catch HttpStatusCodeException

The following gives you an integration test example with MockRestServiceServer

RestClientResponseExceptionTest.java

package com.hellokoding.springcore;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;

public class RestClientResponseExceptionTest {
    private static final String URL = "/server/products/1";
    private MockRestServiceServer mockRestServiceServer;
    private RestTemplate restTemplate;

    @BeforeEach
    public void setUp() {
        restTemplate = new RestTemplate();
        mockRestServiceServer = MockRestServiceServer.createServer(restTemplate);
    }

    @Test
    public void response4xx() {
        mockRestServiceServer
            .expect(requestTo(URL))
            .andExpect(method(HttpMethod.GET))
            .andRespond(withBadRequest());

        ResponseEntity responseEntity = consumeWebService(URL, Object.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }

    @Test
    public void response5xx() {
        mockRestServiceServer
            .expect(requestTo(URL))
            .andExpect(method(HttpMethod.GET))
            .andRespond(withServerError());

        ResponseEntity responseEntity = consumeWebService(URL, Object.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
    }

    <T> ResponseEntity consumeWebService(String url, Class<T> responseType) {
        try {
            return restTemplate.getForEntity(url, responseType);
        } catch (RestClientResponseException e) {
            return ResponseEntity
                .status(e.getRawStatusCode())
                .body(e.getResponseBodyAsString());
        }
    }
}

Implement ResponseErrorHandler

If you don’t like try-catch every time executing a RestTemplate request, try to implement ResponseErrorHandler and add it to the RestTemplate bean

CustomResponseErrorHandler.java

package com.hellokoding.springcore;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Scanner;

@Slf4j
public class CustomResponseErrorHandler implements ResponseErrorHandler {
    @Override
    public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
        HttpStatus status = clientHttpResponse.getStatusCode();
        return status.is4xxClientError() || status.is5xxServerError();
    }

    @Override
    public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
        String responseAsString = toString(clientHttpResponse.getBody());
        log.error("ResponseBody: {}", responseAsString);

        throw new CustomException(responseAsString);
    }

    @Override
    public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
        String responseAsString = toString(response.getBody());
        log.error("URL: {}, HttpMethod: {}, ResponseBody: {}", url, method, responseAsString);

        throw new CustomException(responseAsString);
    }

    String toString(InputStream inputStream) {
        Scanner s = new Scanner(inputStream).useDelimiter("\\A");
        return s.hasNext() ? s.next() : "";
    }

    static class CustomException extends IOException {
        public CustomException(String message) {
            super(message);
        }
    }
}

hasError method is used to indicate whether the given response has any errors

The two handleError methods are used to handle the error in the given response. Only handleError(response) is required to implement, however, it will be overshadowed if you implement handleError(URI url, HttpMethod method, ClientHttpResponse response)

Plug your custom ResponseErrorHandler into a RestTemplate bean

To make your custom ResponseErrorHandler work, you have to register it with a RestTemplate bean

In practice, to follow DRY and leverage the Spring Dependency Injection, you can define your RestTemplate beans in a separate class config and inject it via @Autowired into where it is used such as Spring @Component or @Service class

RestTemplateWithErrorHandlerConfig.java

package com.hellokoding.springcore;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateWithErrorHandlerConfig {
    @Bean
    RestTemplate restTemplateWithErrorHandler() {
        return new RestTemplateBuilder()
            .errorHandler(new CustomResponseErrorHandler())
            .build();
    }
}

ConsumerService.java

package com.hellokoding.springcore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class ConsumerService {
    @Autowired
    RestTemplate restTemplateWithErrorHandler;

    public <T> ResponseEntity consume(String url, Class<T> responseType) {
        return restTemplateWithErrorHandler.getForEntity(url, responseType);
    }
}

The following gives you an integration test example with SpringRunner and MockRestServiceServer

CustomResponseErrorHandlerTest.java

package com.hellokoding.springcore;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.ResponseActions;

import static com.hellokoding.springcore.CustomResponseErrorHandler.CustomException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {RestTemplateWithErrorHandlerConfig.class, ConsumerService.class})
public class CustomResponseErrorHandlerTest {
    private static final String URL = "/server/products/1";
    private ResponseActions responseActions;

    @Autowired
    private ConsumerService consumerService;

    @BeforeEach
    public void setUp() {
        responseActions = MockRestServiceServer.createServer(consumerService.restTemplateWithErrorHandler)
            .expect(requestTo(URL))
            .andExpect(method(HttpMethod.GET));
    }

    @Test
    public void response4xx() {
        responseActions.andRespond(withBadRequest());

        assertThatThrownBy(() -> consumerService.consume(URL, Object.class))
            .hasCauseInstanceOf(CustomException.class);
    }

    @Test
    public void response5xx() {
        responseActions.andRespond(withServerError());

        assertThatThrownBy(() ->  consumerService.consume(URL, Object.class))
            .hasCauseInstanceOf(CustomException.class);
    }
}

Conclusion

In this tutorial, we learned to handle RestTemplate error by catching RestClientResponseException and implementing ResponseErrorHandler. You can find the full source code on GitHub

Follow HelloKoding