I face a challenge when migrating from RestTemplate to RestClient. The documentation mentions the changes in calls, but I have specific exception handling dealing that I have to return to the client (e.g. timeouts, invalid urls, invalid ssl certificate). These corner cases are difficult to test during this migration, and I am afraid I might miss some new ones.

Could you provide a migration guide for the exceptions thrown by RestTemplate, to the exceptions thrown by RestClient? Has anything changed? - Are RestTemplate's HttpStatusCodeException and ResourceAccessException still thrown by RestClient? - Are the underlying exceptions still retrievable?

My project started with Spring Boot 1.5.4 and is now on 3.5.x, so I might also have missed some changes in the meantime.

My code looks mostly like this:

record HttpRequest<T>(HttpMethod method, String url, Map<String, String> headers, T body) {}
record HttpResponse(HttpStatusCode httpStatusCode, Map<String, String> headers, String body) {}

public HttpResponse sendRequest(HttpRequest<?> request)
{
    try
    {
        HttpHeaders headers = new HttpHeaders();
        request.getHeaders().forEach(headers::set);
        HttpEntity<?> requestEntity = new HttpEntity<>(request.getBody(), headers);

        ResponseEntity<String> response = restTemplate.exchange(request.getUrl(), request.getMethod(), requestEntity, String.class);

        return new HttpResponse(response.getStatusCode(), response.getHeaders().toSingleValueMap(), response.getBody());
    }
    catch (HttpStatusCodeException e)
    {
        HttpStatusCode status = e.getStatusCode();

        switch (status.value())
        {
            case 400: { /*...*/}
            case 401: { /*...*/}
            //etc.
        }

        return new HttpResponse(status, e.getResponseHeaders().toSingleValueMap(), e.getResponseBodyAsString());
    }
    catch (ResourceAccessException e)
    {
        Class<?> exceptionClass = e.getRootCause() != null ? e.getRootCause().getClass() : null;
        final HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; 

        ObjectNode body = objectMapper.createObjectNode();

        if (isA(java.net.UnknownHostException.class, exceptionClass))
        {
            // UnknownHostException - Invalid URL (DNS resolve failed)
            body.put("reason", "Invalid URL");
        }
        else if (isA(java.net.ConnectException.class, exceptionClass))
        {
            // ConnectException - Connection refused
            body.put("reason", "Server not connecting. Connection refused.");
        }
        else if (isA(org.apache.hc.client5.http.ConnectTimeoutException.class, exceptionClass))
        {
            // ConnectTimeoutException - Server not connecting within timeout
            body.put("reason", "Server not connecting in a timely fashion");
            body.put("timeout", restProperties.getConnectionRequestTimeout());
        }
        else if (isA(java.net.SocketTimeoutException.class, exceptionClass))
        {
            // SocketTimeoutException - Server not responding within timeout
            body.put("reason", "Server not responding in a timely fashion");
            body.put("timeout", restProperties.getSocketTimeout());
        }
        else if (isA(javax.net.ssl.SSLPeerUnverifiedException.class, exceptionClass))
        {
            // SSLPeerUnverifiedException - Certificate not valid for this domain
            body.put("reason", "Certificate not valid for this domain");
        }
        else
        {
            // Other
            body.put("reason", e.getRootCause() != null ? e.getRootCause().getMessage() : "Unknown root cause");
        }

        return new HttpResponse(httpStatus, null, body.toString());
    }
}

Comment From: donalmurtagh

Could you provide a migration guide for the exceptions thrown by RestTemplate, to the exceptions thrown by RestClient?

That's not really possible because RestClient doesn't necessarily throw an exception when a non 2xx status is returned. It depends on how you submit the request. For example, if you use exchange(), an exception is never thrown, but if you use retrieve() a RestClientException will be thrown for a non 2xx status by default, but this behaviour can be overridden.

All of this is already covered in the RestClient docs, so there's no need for a migration guide.

Comment From: bclozel

@donalmurtagh I met @wtell400 at Devoxx and asked him to create an issue about this. While we tried to stick as much as possible to the existing runtime behavior, it was worth checking and improving the migration guide.

@wtell400 Thanks for your issue and the code snippet above. Because RestClient and RestTemplate share most of their infrastructure, all exceptions thrown by request factories, interceptors and message converters remain the same. If there are any behavior differences, there must be in the DefaultRestClient and RestTemplate (and parent) types.

Using your code snippet and our source code as starting points, I have checked that: * ResourceAccessException and causes are similar in both clients. Causes are thrown by request factories and underlying HTTP libraries, and both RestClient/RestTemplate re-throw them in a similar fashion. * HttpStatusCodeException are thrown by methods similarly as well. RestClient has additional exchange() methods that do provide more control compared to RestTemplate#execute, as pointed out by @donalmurtagh * RestClient is also more flexible for handling HTTP status errors and turning them into exceptions.

In summary, I did not notice significant differences with the exception hierarchy and handling. I did use this opportunity to improve a bit the migration guide in our reference docs.