Error Handling on Java SDK for REST API service

2019-03-12 13:19发布

问题:

We are building a Java SDK to simplify the access to one of our services that provide a REST API. This SDK is to be used by 3rd-party developers. I am struggling to find the best pattern to implement the error handling in the SDK that better fits the Java language.

Let's say we have the rest endpoint: GET /photos/{photoId}. This may return the following HTTP status codes:

  • 401 : The user is not authenticated
  • 403 : The user does not have permission to access this photo
  • 404 : There's no photo with that id

The service looks something like this:

interface RestService {   
    public Photo getPhoto(String photoID);
} 

In the code above I am not addressing the error handling yet. I obviously want to provide a way for the client of the sdk to know which error happened, to potentially recover from it. Error handling in Java is done using Exceptions, so let's go with that. However, what is the best way to do this using exceptions?

1. Have a single exception with information about the error.

public Photo getPhoto(String photoID) throws RestServiceException;

public class RestServiceException extends Exception {
    int statusCode;

    ...
}

The client of the sdk could then do something like this:

try {
    Photo photo = getPhoto("photo1");
}
catch(RestServiceException e) {
    swtich(e.getStatusCode()) {
        case 401 : handleUnauthenticated(); break;
        case 403 : handleUnauthorized(); break;
        case 404 : handleNotFound(); break;
    }
}

However I don't really like this solution mainly for 2 reasons:

  • By looking at the method's signature the developer has no idea what kind of error situations he may need to handle.
  • The developer needs to deal directly with the HTTP status codes and know what they mean in the context of this method (obviously if they are correctly used, a lot of the times the meaning is known, however that may not always be the case).

2. Have a class hierarchy of errors

The method signature remains:

public Photo getPhoto(String photoID) throws RestServiceException;

But now we create exceptions for each error type:

public class UnauthenticatedException extends RestServiceException;
public class UnauthorizedException extends RestServiceException;
public class NotFoundException extends RestServiceException;

Now the client of the SDK could then do something like this:

try {
    Photo photo = getPhoto("photo1");
}
catch(UnauthenticatedException e) {
    handleUnauthorized();
}
catch(UnauthorizedException e) {
    handleUnauthenticated();
}
catch(NotFoundException e) {
    handleNotFound();
}

With this approach the developer does not need to know about the HTTP status codes that generated the errors, he only has to handle Java Exceptions. Another advantage is that the developer may only catch the exceptions he wants to handle (unlike the previous situation where it would have to catch the single Exception (RestServiceException) and only then decide if he wants to deal with it or not).

However, there's still one problem. By looking at the method's signature the developer still has no idea about the kind of errors he may need to handle because we only have the super class in the method's signature.

3. Have a class hierarchy of errors + list them in the method's signature

Ok, so what comes to mind now is to change the method's signature to:

public Photo getPhoto(String photoID) throws UnauthenticatedException, UnauthorizedException, NotFoundException;

However, it is possible that in the future new error situations could be added to this rest endpoint. That would mean adding a new Exception to the method's signature and that would be a breaking change to the java api. We would like to have a more robust solution that would not result in breaking changes to the api in the situation described.

4. Have a class hierarchy of errors (using Unchecked exceptions) + list them in the method's signature

So, what about Unchecked exceptions? If we change the RestServiceException to extend the RuntimeException:

public class RestServiceException extends RuntimeException

And we keep the method's signature:

public Photo getPhoto(String photoID) throws UnauthenticatedException, UnauthorizedException, NotFoundException;

This way I can add new exceptions to the method's signature without breaking existing code. However, with this solution the developer is not forced to catch any exception and won't notice that there are error situations he needs to handle until he carefully reads the documentation (yeah, right!) or noticed the Exceptions that are in the method's signature.

What's the best practice for error handling in these kind of situations?

Are there other (better) alternatives to the ones I mentioned?

回答1:

I've seen libraries that combine your suggestions 2 and 3, e.g.

public Photo getPhoto(String photoID) throws RestServiceException, UnauthenticatedException, UnauthorizedException, NotFoundException;

This way, when you add a new checked exception that extends RestServiceException, you're not changing the method's contract and any code using it still compiles.

Compared to a callback or unchecked exception solution, an advantage is that this ensures your new error will be handled by the client code, even if it's only as a general error. In a callback, nothing would happen, and with an unchecked exception, your client application might crash.



回答2:

Exception handling alternatives: Callbacks

I don't know if it's a better alternative, but you could use callbacks. You can make some methods optional by providing a default implementation. Take a look to this:

    /**
     * Example 1.
     * Some callbacks will be always executed even if they fail or 
     * not, all the request will finish.
     * */
    RestRequest request = RestRequest.get("http://myserver.com/photos/31", 
        Photo.class, new RestCallback(){

            //I know that this error could be triggered, so I override the method.
            @Override
            public void onUnauthorized() {
                //Handle this error, maybe pop up a login windows (?)
            }

            //I always must override this method.
            @Override
            public void onFinish () {
                //Do some UI updates...
            }

        }).send();

This is how the callback class looks like:

public abstract class RestCallback {

    public void onUnauthorized() {
        //Override this method is optional.
    }

    public abstract void onFinish(); //Override this method is obligatory.


    public void onError() {
        //Override this method is optional.
    }

    public void onBadParamsError() {
        //Override this method is optional.
    }

}

Doing something like this you could define an request life-cycle, and manage every state of the request. You can make some methods optional to implement or not. You can get some general errors and give the chance at the user to implements the handling, like in the onError.

How can I define clearly what exceptions handle?

If you ask me, the best approach is draw the life-cycle of the request, something like this:

This is only a poor example, but the important it's keep in mind that all the methods implementation, could be or not, optionals. If onAuthenticationError is obligatory, not neccesarily the onBadUsername will be too, and viceversa. This is the point that makes this callbacks so flexible.

And how I implement the Http client?

Well I don't know much about http clients, I always use the apache HttpClient, but there's not a lot of differences between the http clients, the most have a little more or a little fewer features, but in the end, they are all just the same. Just pick up the http method, put the url, the params, and send. For this example I will use the apache HttpClient

public class RestRequest {
    Gson gson = new Gson();

    public <T> T post(String url, Class<T> clazz,
            List<NameValuePair> parameters, RestCallback callback) {
        // Create a new HttpClient and Post Header
        HttpClient httpclient = new DefaultHttpClient();
        HttpPost httppost = new HttpPost(url);
        try {
            // Add your data
            httppost.setEntity(new UrlEncodedFormEntity(parameters));
            // Execute HTTP Post Request
            HttpResponse response = httpclient.execute(httppost);
            StringBuilder json = inputStreamToString(response.getEntity()
                    .getContent());
            T gsonObject = gson.fromJson(json.toString(), clazz);
            callback.onSuccess(); // Everything has gone OK
            return gsonObject;

        } catch (HttpResponseException e) {
            // Here are the http error codes!
            callback.onError();
            switch (e.getStatusCode()) {
            case 401:
                callback.onAuthorizationError();
                break;
            case 403:
                callback.onPermissionRefuse();
                break;
            case 404:
                callback.onNonExistingPhoto();
                break;
            }
            e.printStackTrace();
        } catch (ConnectTimeoutException e) {
            callback.onTimeOutError();
            e.printStackTrace();
        } catch (MalformedJsonException e) {
            callback.onMalformedJson();
        }
        return null;
    }

    // Fast Implementation
    private StringBuilder inputStreamToString(InputStream is)
            throws IOException {
        String line = "";
        StringBuilder total = new StringBuilder();

        // Wrap a BufferedReader around the InputStream
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));

        // Read response until the end
        while ((line = rd.readLine()) != null) {
            total.append(line);
        }

        // Return full string
        return total;
    }

}

This is an example implementation of the RestRequest. This is only one simple example, theres a lot of topics to discuss when you are making your own rest client. For example, "what kind of json library use to parse?", "are you working for android or for java?" (this is important because I don't know if android supports some features of java 7 like multi-catch exceptions, and there's some technologies that isn't availabe for java or android and viceversa).

But the best that I can say you is code the sdk api in terms of the user, note that the lines to make the rest request are few.

Hope this helps! Bye :]



回答3:

It seems you are doing things by "hand". I would recommend you0 give a try to Apache CXF.

It's a neat implementation the JAX-RS API that enables you to almost forget about REST. It plays well with (also recommended) Spring.

You simply write classes that implement your interfaces (API). What you need to do is to annotate the methods and parameters of your interfaces with JAX-RS annotations.

Then, CXF does the magic.

You throw normal Exceptions in your java code, and then use exception mapper on server/nd or client to translate between them and HTTP Status code.

This way, on server/Java client side, you only deal with regular 100% Java exception, and CXF handles the HTTP for you: You have both the benefits of a clear REST API and a Java Client ready to be used by your users.

The client can either be generated from your WDSL, or built at runtime from introspection of the interface annotations.

See :

  1. http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-Exceptionhandling
  2. http://cxf.apache.org/docs/how-do-i-develop-a-client.html

In our application, we have defined and mapped a set of error codes and their counterpart Exceptions :

  • 4XX Expected / Functional excecption (like bad arguments, empty sets, etc)
  • 5XX Unexpected / Unrecovable RunTimeException for internal errors that "should not happen"

It follows both REST and Java standards.



回答4:

The solution may vary depending on your needs.

  • If it is supposed that there could appear unpredictable new exception types in the future, your second solution with checked exception hierarchy and method that throw their superclass RestServiceException is the best one. All known subclasses should be listed in the javadoc like Subclasses: {@link UnauthenticatedException}, ..., to let developers know what kind of of exceptions there could hide. It should be noticed that if some method could throw only few exceptions from this list, they should be described in the javadoc of this method using @throws.

  • This solution is also applicable in the case when all appearances of RestServiceException means that any of it's subclasses could hide behind it. But in this case, if RestServiceException subclasses hasn't their specific fields and methods, your first solution is preferrable, but with some modifications:

    public class RestServiceException extends Exception {
        private final Type type;
        public Type getType();
        ...
        public static enum Type {
            UNAUTHENTICATED,
            UNAUTHORISED,
            NOT_FOUND;
        }
    }
    
  • Also there is a good practice to create alternative method that will throw unchecked exception that wraps RestServiceException exeption itself for usage within ‘all-or-nothing’ business logic.

    public Photo getPhotoUnchecked(String photoID) {
        try {
            return getPhoto(photoID);
        catch (RestServiceException ex) {
            throw new RestServiceUncheckedException(ex);
        }
    }
    


回答5:

It all comes down to how informative your API error responses are. The more informative the error handling of the API is, the more informative the exception handling can be. I would believe the exceptions would only be as informative as the errors returned from the API.

Example:

{ "status":404,"code":2001,"message":"Photo could not be found."}

Following your first suggestion, if the Exception contained both the status and the API error code, the developer has a better understanding of what he needs to do and more option when it comes to exception handling. If the exception also contained the error message that was returned, as well, the developer shouldn't even need to reference the documentation.