Spring RestTemplate treats Non-200 status as Exception

·

4 min read

Recently I run into the following code snippets:

ResponseEntity<ApmTopoResponse> response2 = null;
try {
  response2 = restTemplate.exchange(topoUrl, HttpMethod.GET, request, ApmTopoResponse.class, 1);
} catch (Exception e) {
  throw new CustomException.WisePaasApmTestingException(e.getMessage());
}

ApmTopoResponse apmTopoResponse = null;
if (response2.getStatusCode() == HttpStatus.OK) {
  apmTopoResponse = response2.getBody();
  result = Boolean.TRUE;
}

I am wondering when will RestTemplate throw an exception and is it necessary to check the http status code before further process? To make things clear, I decide to read the source code of RestTemplate and find this:

@Override
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,
        ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException {

    Type type = responseType.getType();
    RequestCallback requestCallback = httpEntityCallback(requestEntity, type);
    ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(type);
    return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));
}

The exchange method calls execute method, then execute method calls doExecute method:

@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

    Assert.notNull(url, "URI is required");
    Assert.notNull(method, "HttpMethod is required");
    ClientHttpResponse response = null;
    try {
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
            requestCallback.doWithRequest(request);
        }
        response = request.execute();
        handleResponse(url, method, response);
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }
    catch (IOException ex) {
        String resource = url.toString();
        String query = url.getRawQuery();
        resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
        throw new ResourceAccessException("I/O error on " + method.name() +
                " request for \"" + resource + "\": " + ex.getMessage(), ex);
    }
    finally {
        if (response != null) {
            response.close();
        }
    }
}

Obviously, if low-level IO error happens (ex: can't make a http connection with target http server), we will get a ResourceAccessException, otherwise the response will be handled by handleResponse method before it is return.Now let's see this method:

protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
    ResponseErrorHandler errorHandler = getErrorHandler();
    boolean hasError = errorHandler.hasError(response);
    if (logger.isDebugEnabled()) {
        try {
            int code = response.getRawStatusCode();
            HttpStatus status = HttpStatus.resolve(code);
            logger.debug("Response " + (status != null ? status : code));
        }
        catch (IOException ex) {
            // ignore
        }
    }
    if (hasError) {
        errorHandler.handleError(url, method, response);
    }
}

As we can see, it is the errorHandler's responsibility to decide is there any error happens and how to handle it. But where does it come from?

private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
...
/**
 * Set the error handler.
 * <p>By default, RestTemplate uses a {@link DefaultResponseErrorHandler}.
 */
public void setErrorHandler(ResponseErrorHandler errorHandler) {
    Assert.notNull(errorHandler, "ResponseErrorHandler must not be null");
    this.errorHandler = errorHandler;
}

The above codes show that RestTemplate will use a DefaultResponseErrorHandler to do error handling job if I doesn't specify one by setErrorHandler method. RestTemplate.handleResponse method will call errorHandler.hasError method(see above), in DefaultResponseErrorHandler:

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
    int rawStatusCode = response.getRawStatusCode();
    HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
    return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
}
...
protected boolean hasError(HttpStatus statusCode) {
    return statusCode.isError();
}

It uses HttpStatus.isError() to determine whether there is an error :

public boolean isError() {
    return (is4xxClientError() || is5xxServerError());
}

So right now, I am sure that 4xx or 5xx status code will be considered an Error by the default implementation. When it indeed has an error, ResponseErrorHandler's default method will be called (DefaultResponseErrorHandler implements ResponseErrorHandler):

default void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
        handleError(response);
    }

Then the overrided handleError method of DefaultResponseErrorHandler will be called:

@Override
public void handleError(ClientHttpResponse response) throws IOException {
    HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
    if (statusCode == null) {
        byte[] body = getResponseBody(response);
        String message = getErrorMessage(response.getRawStatusCode(),
                response.getStatusText(), body, getCharset(response));
        throw new UnknownHttpStatusCodeException(message,
                response.getRawStatusCode(), response.getStatusText(),
                response.getHeaders(), body, getCharset(response));
    }
    handleError(response, statusCode);
}
...
...
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
    String statusText = response.getStatusText();
    HttpHeaders headers = response.getHeaders();
    byte[] body = getResponseBody(response);
    Charset charset = getCharset(response);
    String message = getErrorMessage(statusCode.value(), statusText, body, charset);

    switch (statusCode.series()) {
        case CLIENT_ERROR:
            throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
        case SERVER_ERROR:
            throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
        default:
            throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
    }
}

Finally we reach the place where an exception comes from. For 4xx series, we will get a HttpClientErrorException; for 5xx series, it will be a HttpServerErrorException. So if we don't want to get an exception just because http status code is not 200, we should implement ResponseErrorHandler interface to make our own ErrorHandler. I think at least we should separate these different kind of exceptions from each other, the hierarchy can be simply shown below:

RestClientException
        |__ResourceAccessException
        |__RestClientResponseException
        |            |__HttpStatusCodeException
        |            |            |__ HttpClientErrorException
        |            |            |__ HttpServerErrorException
        |            |__UnknownHttpStatusCodeException 
        |__UnknownContentTypeException

We can catch different exceptions and do different things accordingly.