Spring RestTemplate Behavior when handling responses with a status of NO_CONTENT

73,197

Solution 1

This should now be fixed in Spring 3.1 RC1.

https://jira.spring.io/browse/SPR-7911

Solution 2

One more way to solve this would be to make response entity as null as shown below.

  ResponseEntity<?> response = restTemplate.exchange("http://localhost:8080/myapp/user/{userID}",
                                                             HttpMethod.DELETE, 
                                                             requestEntity,
                                                             null,
                                                             userID);

If you still need response headers, try implementing the ResponseErrorHandler.

Solution 3

I believe you should probably look at the ResponseExtractor interface & call execute on the RestTemplate providing your implementation of the extractor. To me it looks like a common requirement to do this so have logged this:

https://jira.springsource.org/browse/SPR-8016

Here's one I prepared earlier:

private class MyResponseExtractor extends HttpMessageConverterExtractor<MyEntity> {

    public MyResponseExtractor (Class<MyEntity> responseType,
      List<HttpMessageConverter<?>> messageConverters) {
        super(responseType, messageConverters);
    }

    @Override
    public MyEntity extractData(ClientHttpResponse response) throws IOException {

        MyEntity result;

        if (response.getStatusCode() == HttpStatus.OK) {
            result = super.extractData(response);
        } else {
            result = null;
        }

        return result;
    }
}

I've tested this & it seems to do what I want.

To create the instance of the ResponseExtractor I call the constructor & pass the converters from a RestTemplate instance that's been injected;

E.g.

ResponseExtractor<MyEntity> responseExtractor =
    new MyResponseExtractor(MyEntity.class, restTemplate.getMessageConverters());

Then the call is:

MyEntity responseAsEntity =
    restTemplate.execute(urlToCall, HttpMethod.GET, null, responseExtractor);

Your mileage may vary. ;-)

Solution 4

Here's a simple solution where you can set the default Content-Type for use if it is missing in the response. The Content-Type is added to the response header before it is handed back off to the preconfigured ResponseExtractor for extraction.

public class CustomRestTemplate extends RestTemplate {

    private MediaType defaultResponseContentType;

    public CustomRestTemplate() {
        super();
    }

    public CustomRestTemplate(ClientHttpRequestFactory requestFactory) {
        super(requestFactory);
    }

    public void setDefaultResponseContentType(String defaultResponseContentType) {
        this.defaultResponseContentType = MediaType.parseMediaType(defaultResponseContentType);
    }

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

        return super.doExecute(url, method, requestCallback, new ResponseExtractor<T>() {
            public T extractData(ClientHttpResponse response) throws IOException {
                if (response.getHeaders().getContentType() == null && defaultResponseContentType != null) {
                    response.getHeaders().setContentType(defaultResponseContentType);
                }

                return responseExtractor.extractData(response);
            }
        });
    }
}

Solution 5

Or you could extend RestTemplate and override doExecute(..) and check the response body.

For example here is what I implemented and works for us:

@Override
protected <T> T doExecute(final URI url, final HttpMethod method, final RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor)
        throws RestClientException
{
    Assert.notNull(url, "'url' must not be null");
    Assert.notNull(method, "'method' must not be null");
    ClientHttpResponse response = null;
    try
    {
        final ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null)
        {
            requestCallback.doWithRequest(request);
        }
        response = request.execute();
        if (!getErrorHandler().hasError(response))
        {
            logResponseStatus(method, url, response);
        }
        else
        {
            handleResponseError(method, url, response);
        }
        if ((response.getBody() == null) || (responseExtractor == null))
        {
            return null;
        }
        return responseExtractor.extractData(response);
    }
    catch (final IOException ex)
    {
        throw new ResourceAccessException("I/O error: " + ex.getMessage(), ex);
    }
    finally
    {
        if (response != null)
        {
            response.close();
        }
    }
}
Share:
73,197
AHungerArtist
Author by

AHungerArtist

Rubber duckies.

Updated on July 09, 2022

Comments

  • AHungerArtist
    AHungerArtist almost 2 years

    Okay, I have a class NamedSystems, that has as its only field a Set of NamedSystem.

    I have a method to find NamedSystems by certain criteria. That's not really important. When it gets results, everything works fine. However, when it can't find anything, and thus returns a null (or empty -- I've tried both ways) set, I get problems. Let me explain.

    I'm using the Spring RestTemplate class and I'm making a call like this in a unit test:

    ResponseEntity<?> responseEntity = template.exchange(BASE_SERVICE_URL + "?
      alias={aliasValue}&aliasAuthority={aliasAssigningAuthority}", 
      HttpMethod.GET, makeHttpEntity("xml"), NamedSystems.class, 
      alias1.getAlias(), alias1.getAuthority());
    

    Now, since this would normally return a 200, but I want to return a 204, I have an interceptor in my service that determines if a ModelAndView is a NamedSystem and if its set is null. If so, I then the set the status code to NO_CONTENT (204).

    When I run my junit test, I get this error:

    org.springframework.web.client.RestClientException: Cannot extract response: no Content-Type found
    

    Setting the status to NO_CONTENT seems to wipe the content-type field (which does make sense when I think about it). So why is it even looking at it?

    Spring's HttpMessageConverterExtractor extractData method:

    public T extractData(ClientHttpResponse response) throws IOException {
        MediaType contentType = response.getHeaders().getContentType();
        if (contentType == null) {
            throw new RestClientException("Cannot extract response: no Content-Type found");
        }
        for (HttpMessageConverter messageConverter : messageConverters) {
            if (messageConverter.canRead(responseType, contentType)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Reading [" + responseType.getName() + "] as \"" + contentType
                        +"\" using [" + messageConverter + "]");
                }
                return (T) messageConverter.read(this.responseType, response);
            }
        }
        throw new RestClientException(
            "Could not extract response: no suitable HttpMessageConverter found for response type [" +
            this.responseType.getName() + "] and content type [" + contentType + "]");
    }
    

    Going up the chain a bit to find out where that Extractor is set, I come to RestTemplate's exchange() method that I used in the test:

    public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
      HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException {
        HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
        ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
        return execute(url, method, requestCallback, responseExtractor, uriVariables);
    }
    

    So, it's trying to convert what amounts to nothing because of the supplied response type from the exchange call. If I change the responseType from NamedSystems.class to null, it works as expected. It doesn't try to convert anything. If I had tried to set the status code to 404, it also executes fine.

    Am I misguided, or does this seem like a flaw in RestTemplate? Sure, I'm using a junit right now so I know what's going to happen, but if someone is using RestTemplate to call this and doesn't know the outcome of the service call, they would naturally have NamedSystems as a response type. However, if they tried a criteria search that came up with no elements, they'd have this nasty error.

    Is there a way around this without overriding any RestTemplate stuff? Am I viewing this situation incorrectly? Please help as I'm a bit baffled.