RestTemplate & ResponseErrorHandler: Elegant means of handling errors given an indeterminate return object

AJR :

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.

AJR :

Answering my own question: it turns out that there were fallacies in the question itself.

I was implementing a ResponseErrorHandler because I thought I needed it to parse the response even when that response was returned with a HTTP error code. In fact, that isn't the case.

This answer demonstrates that the response can be parsed into an object by catching a HttpStatusCodeException and otherwise using a standard RestTemplate. That negates the need for a custom ResponseErrorHandler and therefore the need to return an object of ambiguous type. The method that is handed the error can catch the HttpStatusCodeException, try to refresh the access token, and then call itself again via recursion. A counter is required to prevent endless recursion but that can be passed through rather than being a class variable.

The downside is that it still requires error management logic spread around the class, along with plenty of boilerplate code, but it's a lot tidier than the other options.

public UserOrders getUserOrders(User user, Integer methodCallCount) {
    methodCallCount++;
    UserOrders userOrders;
    try {
        userOrders = restTemplate.postForObject(USER_ORDERS_URI, request, UserOrders.class);
    } catch (RestClientException ex) {
        APIError apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
        if (methodCallCount < MAX_METHOD_CALLS) {
            if (apiError.isType(ACCESS_TOKEN_EXPIRED)) {
                refreshVendorAccessTokenInfo(user);
                userOrders = getUserOrders(user, methodCallCount);
            }
        }
    }
    return userOrders;
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=79882&siteId=1