Using a RestTemplate
, I am querying a remote API to return an object either of expected type (if HTTP 2xx) or an APIError (if HTTP 4xx / 5xx).
Because the response object is indeterminate, I have implemented a custom ResponseErrorHandler
and overridden handleError(ClientHttpResponse clientHttpResponse)
in order to extract the APIError when it occurs. So far so good:
@Component
public class RemoteAPI {
public UserOrders getUserOrders(User user) {
addAuthorizationHeader(httpHeaders, user.getAccessToken());
HttpEntity<TokenRequest> request = new HttpEntity<>(HEADERS);
return restTemplate.postForObject(CUSTOMER_ORDERS_URI, request, UserOrders.class);
}
private class APIResponseErrorHandler implements ResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) {
try {
APIError apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
} catch ...
}
}
private void refreshAccessToken(User user) {
addAuthorizationHeader(httpHeaders, user.getAccessSecret());
HttpEntity<TokenRequest> request = new HttpEntity<>(HEADERS);
user.setAccessToken(restTemplate.postForObject(TOKEN_REFRESH_URI, request, AccessToken.class));
}
}
The challenge is that getUserOrders()
, or a similar API call, will occasionally fail with a 'recoverable' error; for instance, the API access token may have expired. We should then make an API call to refreshAccessToken()
before re-attempting getUserOrders()
. Recoverable errors such as these should be hidden from the user until the same ones have occurred multiple times, at which point they are are deemed non-recoverable / critical.
Any errors which are 'critical' (e.g.: second failures, complete authentication failure, or transport layer failures) should be reported to the user as there is no automatic recovery available.
What is the most elegant and robust way managing the error handling logic, bearing in mind that the type of object being returned is not known until runtime?
Option 1: Error object as a class variable with try / catch in each API call method:
@Component
public class RemoteAPI {
private APIError apiError;
private class APIResponseErrorHandler implements ResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) {
try {
this.apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
} catch ...
}
}
public UserOrders getUserOrders(User user) {
try {
userOrders = restTemplate.postForObject(CUSTOMER_ORDERS_URI, request, UserOrders.class);
} catch (RestClientException ex) {
// Check this.apiError for type of error
// Check how many times this API call has been attempted; compare against maximum
// Try again, or report back as a failure
}
return userOrders;
}
}
Pros: Clarity on which method originally made the call
Cons: Use of a class variable for a transient value. Lots of boilerplate code for each method that calls the API. Error handling logic spread around multiple methods.
Option 2: User object as a class variable / Error management logic in the ResponseErrorHandler
@Component
public class RemoteAPI {
private User user;
private class APIResponseErrorHandler implements ResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) {
try {
APIError apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
// Check this.apiError for type of error
// Check how many times this API call has been attempted; compare against maximum
// Try again...
getUserOrders();
...or report back as a failure
} catch ...
}
}
Pros: Error management logic is in one place.
Cons: User object must now be a class variable and handled gracefully, because the User object cannot otherwise be accessible within the ResponseErrorHandler and so cannot pass it to getUserOrders(User)
as before. Need to keep track of how many times each method has been called.
Option 3: Error management logic outside of the RemoteAPI class
Pros: Separates error handling from business logic
Cons: API logic is now in another class
Thank you for any advice.