Exception Handling
About
Exception handling in RestTemplate
is crucial to ensure robust communication between services, especially in distributed systems where network failures, downstream service issues, or unexpected responses are common.
Spring’s RestTemplate
throws runtime exceptions (unchecked) to notify the developer when an error occurs during HTTP interaction. By default, Spring uses the DefaultResponseErrorHandler
which throws exceptions for non-2xx status codes.
Without proper handling, errors like timeouts, 404s, or 500s can crash our service or cause undefined behavior. That’s why it's essential to implement systematic exception handling that can recover gracefully, log appropriately, and surface actionable information.
Types of Exceptions Thrown by RestTemplate
Exception Type
Description
Typical Scenario
HttpClientErrorException
Thrown for HTTP status codes in the 4xx range.
404 Not Found, 400 Bad Request, 401 Unauthorized
HttpServerErrorException
Thrown for 5xx errors from the server.
500 Internal Server Error, 503 Service Unavailable
ResourceAccessException
Thrown when connection errors occur — DNS issue, timeout, no network.
Network unreachable, service down
UnknownHttpStatusCodeException
Thrown for unrecognized or custom status codes not part of standard HTTP.
Non-standard HTTP response codes
RestClientException
(base class)
Base class for all RestTemplate client exceptions.
Catch-all for unknown/unexpected errors
Default Behavior (DefaultResponseErrorHandler)
By default, Spring's RestTemplate
uses DefaultResponseErrorHandler
, which:
Throws exceptions on all 4xx and 5xx HTTP responses.
Does not throw exceptions on 2xx or 3xx responses.
Does not handle the error body unless parsed manually.
Handling Exception
Wrap RestTemplate
calls in try-catch blocks
RestTemplate
calls in try-catch blocksThis is the most basic but explicit form of exception handling. We surround our RestTemplate
calls with try-catch blocks where each catch clause targets a specific category of exceptions such as client errors (4xx), server errors (5xx), or connection timeouts.
public User fetchUser(String userId) {
try {
ResponseEntity<User> response = restTemplate.getForEntity(
"https://user-service/api/users/" + userId, User.class);
return response.getBody();
} catch (HttpClientErrorException.NotFound ex) {
// Handle 404 gracefully
throw new UserNotFoundException(userId);
} catch (HttpServerErrorException ex) {
// Retry logic or circuit-breaker fallback can go here
log.error("Downstream service error: {}", ex.getStatusCode());
throw new ExternalServiceException("User service unavailable");
} catch (ResourceAccessException ex) {
// Timeout or DNS failure
log.warn("Network issue while calling user service: {}", ex.getMessage());
throw new NetworkFailureException("Connection to user service failed");
} catch (RestClientException ex) {
// Catch-all
log.error("Unexpected RestTemplate exception: {}", ex.getMessage(), ex);
throw new InternalIntegrationException("Unexpected error communicating with user service");
}
}
Centralize Handling Using a Utility/Adapter Layer
Instead of littering try-catch blocks across the codebase, enterprises abstract RestTemplate logic into a shared HttpClientHelper
, RestTemplateAdapter
, or HttpIntegrationService
. This class wraps all RestTemplate interactions and handles common concerns like retries, error transformation, timeouts, logging, and telemetry.
@Component
public class RestClientAdapter {
private final RestTemplate restTemplate;
public RestClientAdapter(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
public <T> T get(String url, Class<T> responseType) {
try {
return restTemplate.getForObject(url, responseType);
} catch (HttpStatusCodeException ex) {
String body = ex.getResponseBodyAsString();
log.error("HTTP error from [{}]: {}", url, body);
throw new DownstreamHttpException(ex.getStatusCode(), body);
} catch (ResourceAccessException ex) {
log.error("Timeout or connection error while accessing [{}]: {}", url, ex.getMessage());
throw new NetworkIntegrationException(url, ex);
} catch (RestClientException ex) {
log.error("Unexpected error during HTTP call to [{}]: {}", url, ex.getMessage());
throw new InternalIntegrationException("Unknown RestTemplate error", ex);
}
}
}
User user = restClientAdapter.get("http://user-service/api/users/" + userId, User.class);
Use ResponseErrorHandler
for Global Error Handling
ResponseErrorHandler
for Global Error HandlingSpring's RestTemplate
allows us to plug in a custom ResponseErrorHandler
that can intercept and process HTTP errors globally before they reach our business logic. This means we don’t need to handle HttpStatusCodeException
manually every time.
public class CustomRestTemplateErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().is4xxClientError() ||
response.getStatusCode().is5xxServerError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = response.getStatusCode();
String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
if (statusCode.is4xxClientError()) {
if (statusCode == HttpStatus.NOT_FOUND) {
throw new NotFoundException("Resource not found");
}
throw new ClientErrorException(statusCode, body);
} else if (statusCode.is5xxServerError()) {
throw new ServerErrorException(statusCode, body);
}
}
}
Registering the Handler
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.errorHandler(new CustomRestTemplateErrorHandler())
.build();
}
Handling Deserialization Errors
When using RestTemplate
, after a successful HTTP response (e.g., 200 OK), the response body is typically converted (deserialized) into a Java object using Jackson (or another configured message converter). If the response payload doesn't match the expected Java structure, deserialization fails, usually throwing a HttpMessageConversionException
(like JsonMappingException
, MismatchedInputException
, etc.).
These errors are runtime failures and not caught by the usual ResponseErrorHandler
, since they occur after the response is successfully received, but before the result is returned to the caller.
Strategies to Handle Deserialization Errors
1. Wrap RestTemplate Calls and Catch Jackson Exceptions
This is the first line of defense. Deserialization errors can be caught and handled using try-catch
.
try {
ResponseEntity<UserDto> response = restTemplate.exchange(
url, HttpMethod.GET, null, UserDto.class);
return response.getBody();
} catch (HttpMessageConversionException ex) {
log.error("Deserialization error from URL [{}]: {}", url, ex.getMessage());
throw new InvalidDownstreamPayloadException("Malformed JSON received", ex);
}
Key Exceptions to Catch
HttpMessageNotReadableException
Thrown when the response body can’t be parsed (e.g., bad JSON)
JsonMappingException
Thrown by Jackson when structure mismatches (e.g., missing fields, type errors)
MismatchedInputException
Specific Jackson subtype when input doesn’t match target type
2. Define Resilient DTOs
To make DTOs less sensitive to upstream changes:
Mark all fields as optional using
@JsonInclude(JsonInclude.Include.NON_NULL)
Use
@JsonIgnoreProperties(ignoreUnknown = true)
on class levelAvoid using primitives (
int
,boolean
) if the fields are optionalConsider using
@JsonProperty(required = false)
if necessary
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserDto {
private String id;
private String name;
private Integer age; // Avoid primitive int
}
3. Log the Raw Response on Failure
Logging the raw JSON response helps debugging deserialization issues.
catch (HttpMessageConversionException ex) {
log.error("Deserialization failed. Response body: {}", rawJson, ex);
}
We can read raw response manually if needed:
ResponseEntity<String> rawResponse = restTemplate.getForEntity(url, String.class);
String json = rawResponse.getBody();
try {
UserDto user = objectMapper.readValue(json, UserDto.class);
} catch (JsonProcessingException ex) {
log.error("JSON parse error: {}", json, ex);
throw new InvalidDownstreamPayloadException("Deserialization failed", ex);
}
This technique is also useful when the response is partially malformed but salvageable.
4. Implement Custom Error-Resilient Deserializer
For advanced cases, we can write custom Jackson deserializers:
public class LenientUserDeserializer extends JsonDeserializer<UserDto> {
@Override
public UserDto deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
UserDto user = new UserDto();
user.setId(node.path("id").asText(null));
user.setName(node.path("name").asText(null));
// Add default or fallback if missing
user.setAge(node.path("age").isMissingNode() ? 0 : node.path("age").asInt());
return user;
}
}
Then register it:
@JsonDeserialize(using = LenientUserDeserializer.class)
public class UserDto { ... }
5. Fallback Strategy: Fallback DTO or Raw Map
If payloads are highly dynamic or not fully trusted:
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
url, HttpMethod.GET, null,
new ParameterizedTypeReference<Map<String, Object>>() {});
This allows inspection of the payload without schema enforcement. We can then map it to DTO manually or partially.
Last updated