Martin
Martin

Reputation: 81

Spring REST custom error-object handling in client

Tl;Dr;

I have a REST controller in Spring-Boot, either returning the MyPojo.class, or RESTError.class.

I then call this controller from another Spring Service with RestTemplate and want to either get a MyPojo Object, or a RESTError Object.

Is there any way to do this, other than checking the raw String and then manually parsing?

Detailed description

I have two Spring Services.

Lets call them "server-service" and "client-service". The "client-service" has to retrieve a POJO from the "server-service". If something went wrong (Invalid parameter, missing parameter, etc.) a custom error object is returned, that details the error with a 'developer message' and a more broad 'user message'.

That way, if an error occured, I will be able to display the 'user message' in the ui.

This is only meant as a last resort. I should of course do validation in my "client-service" first, before I call the "server-service". Yet, imagine somebody changing the "server-service" without my knowledge...

"server-service" - Code

MyPojo

@Getter @Setter @Accessors(chain = true)
public class MyPojo extends MyPojoSuper{
    private String someString;

    //hashCode & equals & toString
}

RESTError

Following is the custom error object. It has the errorMessage for the general user and the developerMessage for a developer.

@JsonIgnoreProperties(ignoreUnknown = true)
@Getter @Setter @Accessors(chain = true)
public class RESTError extends MyPojoSuper{

    private String errorMessage, developerMessage;

    //hashCode & equals & toString
}

@ControllerAdvice

The class ExceptionHandlerAdvice will handle all thrown exeptions and return the RESTError.

@ControllerAdvice
@RequestMapping("/error")
@Slf4j
public class ExceptionHandlerAdvice {

    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public RESTError missingServletRequestParameterException(MissingServletRequestParameterException ex) {

    log.info("A parameter['" + ex.getParameterName() +"'] is missing.", ex);
    RESTError error = new RESTError();
    error.setDeveloperMessage(ex.getMessage());
    error.setErrorMessage("The parameter['" + ex.getParameterName() +"'] is missing.");

    return error;
    }
}

"client-service - Code"

The client method calling the "server-service"

public MyPojo getMyPojo(){
    restTemplate.setErrorHandler(new MyCustomResponseErrorHandler());
    return restTemplate.getForObject(getURLWithParams(), MyPojo.class);
}

The RESTError Client side

@JsonIgnoreProperties(ignoreUnknown = true)
@Getter @Setter @Accessors(chain = true)
public class RESTError extends MyPojoSuper{

    private String errorMessage, developerMessage;

    //hashCode & equals & toString
}

MyPojoSuper

@Getter @Setter @Accessors(chain = true)
public class MyPojoSuper{
    public boolean error;

    //equals & hashCode & toString
}

The MyCustomResponseErrorHandler

@Slf4j
public class MyCustomResponseErrorHandler implements ResponseErrorHandler {

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        if (response.getStatusCode() != HttpStatus.OK) {
            log.info("There was an error.");
            return true;
        }
        return false;
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        if (response.getStatusCode() != HttpStatus.OK) {
            log.info("There was an error.");
        }
    }
}

Problem

How do I handle my Custom RESTError?

By using the RestTemplate, I can only parse my JSON once and I can only parse it to one Class (either the MyPojo.class, or RESTError.class). Therefore, I could parse to a String, check the String and then reparse the JSON into the desired format:

public MyPojoSuper getMyPojo() {
    ObjectMapper mapper = new ObjectMapper();
    restTemplate.setErrorHandler(new MyCustomResponseErrorHandler());
    String res = restTemplate.getForObject(getURL(), String.class);

    try {
        if (res.contains("\"error\":true")) {
            return mapper.readValue(res, RESTError.class);
        } else {
            return mapper.readValue(res, MyPojo.class);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return new RESTResponse().setError(true);
}

I do not like this solution and would appreciate any other suggestions.

Upvotes: 2

Views: 2551

Answers (3)

dehasi
dehasi

Reputation: 2773

As far as I know, when you use restTemplate, it throws HttpStatusCodeException if the response code does not equal 2xx. that's why I think you have to catch the exception. Like

    try {
      restTemplate.getForObject(url, MyPojo.class);
     } catch (HttpStatusCodeException e) {
         if (e.getStatusCode() == 404) {

           String body = e.getResponseBodyAsString();

           // do smth with error json
        }
    }

Upvotes: 0

Martin
Martin

Reputation: 81

Based on the comment by @Andrew S, here is a solution:

One can register a ResponseErrorHandler by creating a class implementing ResponseErrorHandler in a custom class and registering it with the RestTemplate:

@Slf4j
public class MyCustomResponseErrorHandler implements ResponseErrorHandler {

    private ObjectMapper mapper = new ObjectMapper();

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        if (response.getStatusCode() != HttpStatus.OK) {
            log.info("loggin stuff");
            return true;
        }
    return false;
    }

    @Override
    public void handleError(ClientHttpResponse response) throws     JsonParseException, JsonMappingException, IOException{
        if (response.getStatusCode() != HttpStatus.OK) {
            log.info("logging stuff");
            throw new RestErrorExeption(mapper.readValue(response.getBody(),     RESTError.class));
        }
    }
}

As shown this class now throws a custom exception, the RestErrorExeption. It contains a RESTError object. One can then parse the JSON in the response body by invoking mapper.readValue(response.getBody(), RESTError.class). (Mapper is an instance of com.fasterxml.jackson.databind.ObjectMapper)

One can then catch the exception and get the parsed JSON as a POJO from the RestErrorExeption:

public MyPojoSuper getMyPojo() {
    restTemplate.setErrorHandler(new MyCustomResponseErrorHandler());

    try {
        return restTemplate.getForObject(getURLWithParams(), MyPojo.class);
    } catch (RestErrorExeption e) {
        log.info(e.getMessage());
        return e.getRestError();
    }
}

Please note, that the RESTError.class and the MyPojo.class need the MyPojoSuper.class as their parent to be properly handled.

Upvotes: 1

JekBP
JekBP

Reputation: 81

Your RESTError class is a subclass of MyPojoSuper class

RESTError extends MyPojoSuper

And in your try-catch, you are specifically catching IOException

try {
        if (res.contains("\"error\":true")) {
            return mapper.readValue(res, RESTError.class);
        } else {
            return mapper.readValue(res, MyPojo.class);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

Can you share your class code for MyPojoSuper.java please? Should it be at least subclass of IOException so that it can be caught in your try catch.

If I understood your problem correctly.

Upvotes: 0

Related Questions