StolenKitten
StolenKitten

Reputation: 357

Can a JAX-RS client throw a custom exception type?

I want consumers of my service's client to get more meaningful exceptions than standard JAX-RS WebApplicationExceptions, so they can do things like retry or not based on the cause of the 500 exception.

I've got an ExceptionMapper on the service side which puts the actual exception as the entity of the response in the returned exception, like this:

public Response toResponse(InternalException ex) {
    return Response.status(ex.getStatus)
        .entity(ex)
        .type(MediaType.APPLICATION_JSON_TYPE)
        .build();
}

Which seems to work. The problem is that in the client, even if I have a ClientResponseFilter which extracts the InternalException from the response and throws it, JAX-RS wraps it in a ProcessingException, which defies the entire point of this!

Testcase: badRequest_clientCall_modeledExceptionThrown took 0.272 sec
    Caused an ERROR
Unexpected exception, expected<not.actual.package.InternalException> but was<javax.ws.rs.ProcessingException>
java.lang.Exception: Unexpected exception, expected<not.actual.package.InternalException> but was<javax.ws.rs.ProcessingException>
Caused by: javax.ws.rs.ProcessingException
    at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:263)
    at org.glassfish.jersey.client.JerseyInvocation$3.call(JerseyInvocation.java:709)
    <Incredibly secret stuff ommitted>
Caused by: not.actual.package.InternalException

How do I actually get this client to throw the right exception, short of wrapping the final output in a try/catch (which I'd hate to do because it would complicate retry logic)?

Upvotes: 1

Views: 1882

Answers (1)

Bejond
Bejond

Reputation: 1198

You can use AOP(JaveEE APO example) of spring or JavaEE. Use @AroundInvoke to invoke your service method. Just throw exception in your service method. I suggest you create your own Exceptions like UsernameInvalidException, ParameterErrorException Here is the code example for JavaEE:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManager;
import java.io.IOException;
import java.lang.reflect.Field;

@YourExceptionHandler
@Interceptor
public class YourExceptionInterceptor {

    @Inject
    private Logger logger;

    @AroundInvoke
    public Object handleException(InvocationContext ctx) {

        //logger.debug("hash:{}", System.identityHashCode(this));
        Result returnResult = new Result();

        Field resultField = null;
        Object result = null;
        Class<? extends Object> returnType = ctx.getMethod().getReturnType();

        try
        {
            logger.info("method:{},return type:{}", ctx.getMethod(), 
                ctx.getMethod().getGenericReturnType());
            returnType = ctx.getMethod().getReturnType();
            result = ctx.proceed(); // this invoke your service
        }
        catch ( UsernameInvalidException e )
        {
            try
            {
                result = returnType.newInstance();
                resultField = result.getClass().getDeclaredField("result");

                if ( resultField == null )
                {
                    return null;
                }

                returnResult.setResultType(ResultType.ERROR);
                returnResult.setResultCode(e.getErrorCode()); // code you defined
             // error text you set in UsernameInvalidException when thrown
                returnResult.setText(e.getMessage()); 
            }
            catch ( Exception e1 )
            {
                e1.printStackTrace();
            }
        }
        catch ( Exception e ) // catch other unexpected exceptions
        {
            e.printStackTrace();
        }

        try
        {
            if ( resultField != null )
            {
                resultField.setAccessible(true);
                resultField.set(result, returnResult);
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }

        return result;
    }

}

Defind your exception:

public class BaseException extends RuntimeException {

    protected static final String SCANPAY_EXCEPTION_CODE = "300";

    protected static final String BASE_EXCEPTION_CODE = "400";        

    protected static final String USERNAME_INVALID_EXCEPTION_CODE = "405";

    protected static final String DAO_EXCEPTION_CODE = "401";

    protected static final String SERVICE_NOT_AVAILABLE_CODE = "414";

    protected static final String PARAMETER_ERROR_CODE = "402";

    protected String errorCode;

    public BaseException() {

        initErrorCode();
    }

    public BaseException(String message) {

        super(message);
        initErrorCode();
    }

    public BaseException(Throwable cause) {

        super(cause);
        initErrorCode();
    }

    public BaseException(String message, Throwable cause) {

        super(message, cause);
        initErrorCode();
    }

    public BaseException(String message, Throwable cause,
        boolean enableSuppression, boolean writableStackTrace) {

        super(message, cause, enableSuppression, writableStackTrace);
        initErrorCode();
    }

    protected void initErrorCode() {

        errorCode = BASE_EXCEPTION_CODE;
    }

    public String getErrorCode() {

        return errorCode;
    }

}

Your UsernameInvalidExcepton:

@SuppressWarnings("serial")
public class UsernameInvalidExcepton extends BaseException {

    @Override
    @SuppressWarnings("static-access")
    protected void initErrorCode() {

        this.errorCode = this.USERNAME_INVALID_EXCEPTIO_CODE;
    }

    public UsernameInvalidException(String message) {

        super(message);
    }

Define your own ExceptionHandler, which is an annotation(refer here for custom annotation):

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.interceptor.InterceptorBinding;

@InterceptorBinding
@Inherited
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@Documented
public @interface YourExceptionHandler {

}

Define an exception throw factory(This factory is not necessary, it is just use to throw new different exceptions. You can new exception when you need to throw it.):

public abstract class YourExceptionThrowFactory {

    private YourExceptionThrowFactory() {

    }

    public static void throwUsernameInvalidException(String message) {

        throw new UsernameInvalidException(message);
    }

How to use it? In your service method, use the annotation:

@YourExceptionHandler
public UserInfo getUserInfo(String username) {
   // your service code
   if (username == null) {
    YourExceptionThrowFactory.throwUsernameInvalidException(
        "Username is not valid");  // throw your exception with your message.
}
}

Then for client, it will get your error code and your error message.

All the code above is for JavaEE, if you use spring, you will find corresponding annotations and technology.

Upvotes: 0

Related Questions