How to parse the response body in Java, when the HTTP request has return status 401

67,308

Solution 1

Try the following approach without needing a custom handler. The idea is to get the response as a string from the HttpStatusCodeException, and then you can convert it to your object. For the conversion I used the Jackson's ObjectMapper:

        try {

            restTemplate.postForObject(url, pojoInstance, responseClass);

        } catch (HttpStatusCodeException e) {

            if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {

                String responseString = e.getResponseBodyAsString();

                ObjectMapper mapper = new ObjectMapper();

                CustomError result = mapper.readValue(responseString,
                        CustomError.class);
            }
        }

Update: Usage of a different factory may also help since there is a bug in the default one related to your issue (see comment below):

RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());

Solution 2

I did it like that:

@Component
public class RestTemplateFactory {
public enum Type {
    JSON, XML
}

public RestTemplate create(Type type) {
    RestTemplate restTemplate = new RestTemplate();
    if (type == Type.XML) {
        Jaxb2RootElementHttpMessageConverter jaxbMessageConverter = new Jaxb2RootElementHttpMessageConverter();
        jaxbMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.TEXT_HTML, MediaType.APPLICATION_XML));
        restTemplate.setMessageConverters(Lists.newArrayList(jaxbMessageConverter));
    }
    restTemplate.setErrorHandler(new BpmRestErrorHandler(restTemplate.getMessageConverters()));
    return restTemplate;
}

public HttpHeaders contentHeaders(Type type) {
    HttpHeaders headers = new HttpHeaders();
    if (type == Type.XML) {
        headers.setContentType(MediaType.APPLICATION_XML);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
    } else {
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    }
    return HttpHeaders.readOnlyHttpHeaders(headers);
}
}

And handler:

public class BpmRestErrorHandler extends DefaultResponseErrorHandler {

private final List<HttpMessageConverter<?>> messageConverters;

public BpmRestErrorHandler(List<HttpMessageConverter<?>> messageConverters) {
    this.messageConverters = messageConverters;
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {
    for (HttpMessageConverter messageConverter : messageConverters) {
        if (messageConverter.canRead(RestRuntimeException.class, response.getHeaders().getContentType())) {
            RestRuntimeExceptionData exceptionData =
                    (RestRuntimeExceptionData)messageConverter.read(RestRuntimeException.class, response);
            throw new BpmRestException(exceptionData);
        }
    }
    super.handleError(response);
}
}

Where RestRuntimeExceptionData is my custom WebFault object. It re-uses RestTemplate's HttpConverters.

Solution 3

I did using clientHttpResponse only with mapper. ClientHttpResponse.getBody() is the stream similar to HttpStatusCodeException.getResponseBodyAsString.

@Component
public class xhandler extends RestTemplateErrorHandler{


    @Override
    protected boolean handleServiceSpecificError(ClientHttpResponse response) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            CustomError error =mapper.readValue(response.getBody(), CustomError.class);
            throw new RestClientException(“x Service returned an error response with error-code: "+response.getStatusCode().toString() + error.getErrorMessage());
        } catch (IOException e) {
            LOG.error(create(this.getClass(), "002"), “x Service returned an error, but the error response could not be parsed: {}", e.toString(), e);
        }
        return false;
    }

}
Share:
67,308
Ivaylo Slavov
Author by

Ivaylo Slavov

I am a self-taught developer (with high-school informatics background). I have experience primarily with the C# and Java languages, as well as web oriented technologies. Currently I work as a senior full-stack developer at Live Interactive Software, where I employ my Java and JavaScript/TypeScript skills. In the area of software development, I am interested in enterprise solutions, distributed systems, modular software architectures and scalable cloud solutions. Alongside the fields in my professional orientation I am interested in psychology, which my current field of study at Sofia University. My hobby interests include marketing and finances, social relations, physics, biology, health and fitness, and religion. Sometimes I am composing music in my free time using audio software, as unfortunately, I am not good at playing any music instrument.

Updated on February 22, 2021

Comments

  • Ivaylo Slavov
    Ivaylo Slavov over 3 years

    I am consuming a RESTful JSON API using Spring's RestTemplate and Jackson. In some cases we may receive a Status 401 (Unauthorized) response with a custom JSON body, that is defined by the API manufacturer, and looks like this:

    {
        "code": 123,
        "message": "Reason for the error"
    }
    

    We need to parse the body, and use the code property in our business logic.

    This is the error response Java object we need to parse to:

    public class CustomError {
    
        @JsonProperty
        private Integer code;
        @JsonProperty
        private String message;
    
        public Integer getCode() {
           return code;
        }
        public String getMessage() {
            return message;
        }
    }
    

    And a custom error handler to do this:

    public class CustomErrorHandler extends DefaultResponseErrorHandler {
        private RestTemplate restTemplate;
        private ObjectMapper objectMapper;
        private MappingJacksonHttpMessageConverter messageConverter;
    
    
        @Override
        public boolean hasError(ClientHttpResponse response) throws IOException {
            return super.hasError(response);
        }
    
        @Override
        public void handleError(final ClientHttpResponse response) throws IOException {
    
            try {
                CustomError error = 
                    (CustomError) messageConverter.read(CustomError.class, response);
                throw new CustomErrorIOException(error, error.getMessage());
            } catch (Exception e) {
                // parsing failed, resort to default behavior
                super.handleError(response);
            }
        }
    }
    

    The error handler fails with an HttpMessageNotReadableException in the try block:

    "Could not read JSON: cannot retry due to server authentication, in streaming mode"

    This is how I am sending requests:

    restTemplate.postForObject(url, pojoInstance, responseClass);
    

    If the same request is executed with a plain old rest client program, like Postman, the expected JSON response is received. So, I assume the problem could be with the Spring's ClientHttpResponse implementation somehow not allowing access to the response body, in case of the 401 status.

    Is it indeed possible to parse the response body?

    Update

    From what I investigated, the RestTemplate class uses ClientHttpResponse which in turn creates an sun.net.www.protocol.http.HttpURLConnection that provides the input stream. It is there, where the input stream is being neglected and an IOException is thrown:

    cannot retry due to server authentication, in streaming mode

    So, the HttpURLConnection's implementation is causing the issue.

    Will it be possible to avoid this problem? Perhaps we should use an alternative implementation that does not ignore the response body in case of an error status code? Can you recommend any alternatives?

  • Nikolay
    Nikolay over 9 years
    I tried this solution, but it seems not to work. The exception is actually thrown but the response stream contains no data. If I try the same request with POSTMAN the response body contains the body, so it seems again a RestTemplate problem.
  • Marios
    Marios over 9 years
    I found a bug that describes the problem you have in jira.spring.io/browse/SPR-9999 . It is from last year but perhaps it has not been fixed yet. It suggest to use a different factory so that another client implementation will be used (RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
  • Ivaylo Slavov
    Ivaylo Slavov over 9 years
    @Marios, thank you for your efforts. I was using SimpleClientHttpConnectionFactory before. Your solution works for me when using HttpComponentsClientHttpRequestFactory - perhaps the above mentioned sun.net.www.protocol.http.HttpURLConnection is not used by it.
  • Marios
    Marios over 9 years
    Ivaylo, that's the case indeed, SimpleClientHttpConnectionFactory uses java.net implementation for HTTP requests, while HttpComponentsClientHttpRequestFactory uses Apache's HttpClient underneath
  • Tristan
    Tristan over 6 years
    Just a note : HttpStatusCodeException only gives u syntaxic sugar around HTTP status codes, but you have what you need in RestClientResponseException with "getRawStatusCode()" and "getResponseBodyAsString()".
  • Ivaylo Slavov
    Ivaylo Slavov almost 4 years
    Thanks for your input and the elaborate answer :). I notice that you never actually throw the RestRuntimeException, and it makes me wonder if it is better to use a simple POJO to hold the error data. My concern is that, at construction time, the Exception object gathers the stacktrace, which is unnecessary slowdown. You already have the same stacktrace in the BpmRestException
  • Olegdelone
    Olegdelone almost 4 years
    Actually this is just a complex pojo. It is confusing called, agree. Real exception is - BpmRestException
  • Ivaylo Slavov
    Ivaylo Slavov almost 4 years
    Yep, if that's a POJO then all is fine. I'll rename the class (at least in the post). Exceptions usually expose a constructor accepting another exception (a cause) and one may get easily confused as I did.