Skip to content
Matija Mazi edited this page Mar 5, 2018 · 12 revisions

Basic exception handling in rescu

As a rule of thumb, you should declare throws IOException on all service interface methods. If you don't, everything will still work in normal circumstances, but any I/O exceptions (and any other checked exceptions) that happen will be wrapped in UndeclaredThrowableExceptions, which will make them more difficult and error-prone to handle. Note that since you're communicating with an external server, I/O exception conditions (like no connection, server down etc.) may happen often and it will pay off to put some effort into handling them properly.

Mapping HTTP errors to exceptions

Rescu provides a declarative way to map exceptional responses to Java exceptions: when the REST server returns a response with an non-OK HTTP response code (and under some other conditions, see below), you can have your proxy client throw an exception. If you use custom exception types and the server provides a json dictionary in the error response body, rescu can populate the exception's fields with the json data.

A minimal example

Just add a catch HttpStatusIOException block to your method calls. The HttpStatusIOException will provide you with the HTTP status code and response body, if you need them.

try {
    myServiceProxy.callMethod(...);
} catch (HttpStatusIOException e) {
    log.warn("HTTP error code returned from server: " + e.getHttpStatusCode());
} catch (IOException e) {
    ...
}

A contrived example

Say your server, on unsuccessful authentication, returns HTTP status code 401 and the following json:

{"success":false, "msg":"Incorrect username or password."}

(On a successful call, the json would be differently structured -- we're not covering this here). Then you might create a custom exception type (unnecessary details omitted):

public class MyException extends RuntimeException {
      @JsonProperty("success") private Boolean success;
      @JsonProperty("msg")     private String msg;

      public Boolean getSuccess() { return success; }
      public String getMsg() { return msg; }
      @Override public String getMessage() { return msg; }
}

.. and declare your method like this:

public MyResult doSomething(...) throws IOException, MyException;

This is all you need to do. When the server returns a non-OK (non-2xx) HTTP response code, rescu will parse the response body into a MyException object and throw it so you can catch it where you call the doSomething method and handle it properly; you can access the success and msg properties on the exception.

try {
    MyResult result = myServiceProxy.doSomething(...);
} catch (MyException e) {
    log.warn("Error message returned from server: " + e.getMsg());
} catch (IOException e) {
    ...
}

Note that you should still declare IOException in the method signature (and handle it) for the same reasons as stated above.

Triggering exception parsing based on json content

Rescu will try parsing the response body as exception under any one of the following conditions:

  • HTTP response code is not 2xx, or
  • normal parsing fails with specific exceptions: ExceptionalReturnContentException or JsonMappingException.

Here's how to make rescu parse response as exception even on 200 HTTP response codes when the json response is not what you normally expect.

Say your API method returns a Jackson-annotated custom type Ticker. You need to create a constructor with Jackson-annotated parameters for this type that checks if all the data you need is present, and throws an ExceptionalReturnContentException if not:

public class Ticker {
    private Long last;
    private Long volume;

    public Ticker(
          @JsonProperty("last") Long last,
          @JsonProperty("volume") Long volume
    ) {
        if (last == null || volume == null)
            throw new ExceptionalReturnContentException("Last and volume required.");
        this.last = last;
        this.volume = volume;
    }
}

When constructing the return object from response body, Jackson will call this constructor. If ExceptionalReturnContentException is thrown, rescu will catch it and re-parse the body as exception.

Real-life examples

For further examples, see the XChange project, eg. the ANXV2 service and its exception type.

Assumptions and limitations

  • Your custom exception type must be a subclass of RuntimeException.
  • A single Exception type is supported per method (ie. no mapping from HTTP error codes to different exception types etc.).

Details

  • You can prevent rescu from throwing exceptions on non-2xx HTTP response codes by setting rescu.http.ignoreErrorCodes = true in rescu.properties; this will force rescu to ignore the HTTP response code when deciding whether to parse the response body as exception or as regular return type.
  • You don't need to provide a custom exception type. Omitting it may be useful eg. if the server returns no data in the error response body, or if you don't care about this data. In this case you can simply declare your method as throws HttpStatusIOException (this will provide you with the HTTP response status code and the HTTP response body), or even just throws IOException in case you don't even care about the status code and response body.
  • If parsing of the response body into your custom exception fails for any reason, rescu will throw a HttpStatusIOException that will enable you to access the HTTP response code and body. This is another reason it's wise to declare throws IOException on all methods.
  • You can extend HttpStatusExceptionSupport when creating your custom exception class. This is an easy way to automatically get the HTTP status code in your exception as a property and it will be added to your exception message. As a rule of thumb, if you don't need your own exception class hierarchy, you should just extend HttpStatusExceptionSupport to gain this additional info for free.

Invocation-aware Exceptions

This functionality can be used if you need access to the HTTP API method invocation information in the exceptions.

If your exceptions implement InvocationAware, rescu will populate them with the RestInvocation instance that carries information about the request such as the url, the HTTP method, parameter values etc.

If your exceptions don't implement InvocationAware, you can set the rescu.http.wrapUnexpectedExceptions property in rescu.properties to true and your exceptions will be wrapped in an exception that will carry the RestInvocation instance.

Response-aware Exceptions

If your exceptions implement HttpResponseAware, rescu will populate them with response header information.