Handling custom error response in JAX-RS 2.0 client library

63,132

Solution 1

I believe you want to do something like this:

Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) {
    System.out.println( response.getStatusType() );
    return null;
}

return response.readEntity( MyEntity.class );

Another thing you can try (since I don't know where this API puts stuff -- i.e. in the header or entity or what) is:

Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) {
    // if they put the custom error stuff in the entity
    System.out.println( response.readEntity( String.class ) );
    return null;
}

return response.readEntity( MyEntity.class );

If you would like to generally map REST response codes to Java exception you can add a client filter to do that:

class ClientResponseLoggingFilter implements ClientResponseFilter {

    @Override
    public void filter(final ClientRequestContext reqCtx,
                       final ClientResponseContext resCtx) throws IOException {

        if ( resCtx.getStatus() == Response.Status.BAD_REQUEST.getStatusCode() ) {
            throw new MyClientException( resCtx.getStatusInfo() );
        }

        ...

In the above filter you can create specific exceptions for each code or create one generic exception type that wraps the Response code and entity.

Solution 2

There are other ways to getting a custom error message to the Jersey client besides writing a custom filter. (although the filter is an excellent solution)

1) Pass error message in an HTTP header field. The detail error message could be in the JSON response and in an additional header field, such as "x-error-message".

The Server adds the HTTP error header.

ResponseBuilder rb = Response.status(respCode.getCode()).entity(resp);
if (!StringUtils.isEmpty(errMsg)){
    rb.header("x-error-message", errMsg);
}
return rb.build();

The Client catches the exception, NotFoundException in my case, and reads the response header.

try {
    Integer accountId = 2222;
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("http://localhost:8080/rest-jersey/rest");
    webTarget = webTarget.path("/accounts/"+ accountId);
    Invocation.Builder ib = webTarget.request(MediaType.APPLICATION_JSON);
    Account resp = ib.get(new GenericType<Account>() {
    });
} catch (NotFoundException e) {
    String errorMsg = e.getResponse().getHeaderString("x-error-message");
    // do whatever ...
    return;
}

2) Another solution is to catch the exception and read the response content.

try {
    // same as above ...
} catch (NotFoundException e) {
    String respString = e.getResponse().readEntity(String.class);
    // you can convert to JSON or search for error message in String ...
    return;
} 

Solution 3

The class WebApplicationException was designed for that but for some reason it ignores and overwrites what you specify as parameter for the message.

For that reason I created my own extension WebAppException that honors the parameters. It is a single class and it doesn't require any response filter or a mapper.

I prefer exceptions than creating a Response as it can be thrown from anywhere while processing.

Simple usage:

throw new WebAppException(Status.BAD_REQUEST, "Field 'name' is missing.");

The class:

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.core.Response.StatusType;

public class WebAppException extends WebApplicationException {
    private static final long serialVersionUID = -9079411854450419091L;

    public static class MyStatus implements StatusType {
        final int statusCode;
        final String reasonPhrase;

        public MyStatus(int statusCode, String reasonPhrase) {
            this.statusCode = statusCode;
            this.reasonPhrase = reasonPhrase;
        }

        @Override
        public int getStatusCode() {
            return statusCode;
        }
        @Override
        public Family getFamily() {
            return Family.familyOf(statusCode);
        }
        @Override
        public String getReasonPhrase() {
            return reasonPhrase;
        }
    }

    public WebAppException() {
    }

    public WebAppException(int status) {
        super(status);
    }

    public WebAppException(Response response) {
        super(response);
    }

    public WebAppException(Status status) {
        super(status);
    }

    public WebAppException(String message, Response response) {
        super(message, response);
    }

    public WebAppException(int status, String message) {
        super(message, Response.status(new MyStatus(status, message)). build());
    }

    public WebAppException(Status status, String message) {
        this(status.getStatusCode(), message);
    }

    public WebAppException(String message) {
        this(500, message);
    }

}

Solution 4

A much more concise solution for anyone stumbling on this:

Calling .get(Class<T> responseType) or any of the other methods that take the result type as an argument Invocation.Builder will return a value of the desired type instead of a Response. As a side effect, these methods will check if the received status code is in the 2xx range and throw an appropriate WebApplicationException otherwise.

From the documentation:

Throws: WebApplicationException in case the response status code of the response returned by the server is not successful and the specified response type is not Response.

This allows to catch the WebApplicationException, retrieve the actual Response, process the contained entity as exception details (ApiExceptionInfo) and throw an appropriate exception (ApiException).

public <Result> Result get(String path, Class<Result> resultType) {
    return perform("GET", path, null, resultType);
}

public <Result> Result post(String path, Object content, Class<Result> resultType) {
    return perform("POST", path, content, resultType);
}

private <Result> Result perform(String method, String path, Object content, Class<Result> resultType) {
    try {
        Entity<Object> entity = null == content ? null : Entity.entity(content, MediaType.APPLICATION_JSON);
        return client.target(uri).path(path).request(MediaType.APPLICATION_JSON).method(method, entity, resultType);
    } catch (WebApplicationException webApplicationException) {
        Response response = webApplicationException.getResponse();
        if (response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
            throw new ApiException(response.readEntity(ApiExceptionInfo.class), webApplicationException);
        } else {
            throw webApplicationException;
        }
    }
}

ApiExceptionInfo is custom data type in my application:

import lombok.Data;

@Data
public class ApiExceptionInfo {

    private int code;

    private String message;

}

ApiException is custom exception type in my application:

import lombok.Getter;

public class ApiException extends RuntimeException {

    @Getter
    private final ApiExceptionInfo info;

    public ApiException(ApiExceptionInfo info, Exception cause) {
        super(info.toString(), cause);
        this.info = info;
    }

}
Share:
63,132

Related videos on Youtube

Chuck M
Author by

Chuck M

Java developer and sometimes defender.

Updated on July 25, 2020

Comments

  • Chuck M
    Chuck M almost 4 years

    I am starting to use the new client API library in JAX-RS and really loving it so far. I have found one thing I cannot figure out however. The API I am using has a custom error message format that looks like this for example:

    {
        "code": 400,
        "message": "This is a message which describes why there was a code 400."
    } 
    

    It returns 400 as the status code but also includes a descriptive error message to tell you what you did wrong.

    However the JAX-RS 2.0 client is re-mapping the 400 status into something generic and I lose the good error message. It correctly maps it to a BadRequestException, but with a generic "HTTP 400 Bad Request" message.

    javax.ws.rs.BadRequestException: HTTP 400 Bad Request
        at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:908)
        at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:770)
        at org.glassfish.jersey.client.JerseyInvocation.access$500(JerseyInvocation.java:90)
        at org.glassfish.jersey.client.JerseyInvocation$2.call(JerseyInvocation.java:671)
        at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
        at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
        at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
        at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:424)
        at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:667)
        at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:396)
        at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:296)
    

    Is there some sort of interceptor or custom error handler that can be injected so that I get access to the real error message. I've been looking through documentation but can't see any way of doing it.

    I am using Jersey right now, but I tried this using CXF and got the same result. Here is what the code looks like.

    Client client = ClientBuilder.newClient().register(JacksonFeature.class).register(GzipInterceptor.class);
    WebTarget target = client.target("https://somesite.com").path("/api/test");
    Invocation.Builder builder = target.request()
                                       .header("some_header", value)
                                       .accept(MediaType.APPLICATION_JSON_TYPE)
                                       .acceptEncoding("gzip");
    MyEntity entity = builder.get(MyEntity.class);
    

    UPDATE:

    I implemented the solution listed in the comment below. It is slightly different since the classes have changed a bit in the JAX-RS 2.0 client API. I still think it is wrong that the default behavior is to give a generic error message and discard the real one. I understand why it wouldn't parse my error object, but the un-parsed version should have been returned. I end up having the replicate exception mapping that the library already does.

    Thanks for the help.

    Here is my filter class:

    @Provider
    public class ErrorResponseFilter implements ClientResponseFilter {
    
        private static ObjectMapper _MAPPER = new ObjectMapper();
    
        @Override
        public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
            // for non-200 response, deal with the custom error messages
            if (responseContext.getStatus() != Response.Status.OK.getStatusCode()) {
                if (responseContext.hasEntity()) {
                    // get the "real" error message
                    ErrorResponse error = _MAPPER.readValue(responseContext.getEntityStream(), ErrorResponse.class);
                    String message = error.getMessage();
    
                    Response.Status status = Response.Status.fromStatusCode(responseContext.getStatus());
                    WebApplicationException webAppException;
                    switch (status) {
                        case BAD_REQUEST:
                            webAppException = new BadRequestException(message);
                            break;
                        case UNAUTHORIZED:
                            webAppException = new NotAuthorizedException(message);
                            break;
                        case FORBIDDEN:
                            webAppException = new ForbiddenException(message);
                            break;
                        case NOT_FOUND:
                            webAppException = new NotFoundException(message);
                            break;
                        case METHOD_NOT_ALLOWED:
                            webAppException = new NotAllowedException(message);
                            break;
                        case NOT_ACCEPTABLE:
                            webAppException = new NotAcceptableException(message);
                            break;
                        case UNSUPPORTED_MEDIA_TYPE:
                            webAppException = new NotSupportedException(message);
                            break;
                        case INTERNAL_SERVER_ERROR:
                            webAppException = new InternalServerErrorException(message);
                            break;
                        case SERVICE_UNAVAILABLE:
                            webAppException = new ServiceUnavailableException(message);
                            break;
                        default:
                            webAppException = new WebApplicationException(message);
                    }
    
                    throw webAppException;
                }
            }
        }
    }
    
    • marathon
      marathon over 9 years
      This has frustrated me with jersey. Thank you for posting your ErrorResponseFilter class. What type is ErrorResponse? I cannot figure out where it is coming from.
    • ccleve
      ccleve over 9 years
      Your filter approach is good. You've identified a clear design error in jax rs clients. Resteasy has the same problem.
    • Alexandr
      Alexandr almost 5 years
      @ChuckM, in your solution, original rest exception is wrapped by ResponseProcessingException, which is not good. As an alternative approach, without filters, you can extract response body from the exception.
  • Chuck M
    Chuck M about 10 years
    If I call builder.get(), then it returns a Response object and doesn't automatically throw the remapped exception. I was wondering if there was a more global way to handle this? Some sort of interceptor that could check for status codes, unmarshall the error object and map to an exception type like BadRequestException. I have 30 different API calls so I'd like to handle this more globally.
  • robert_difalco
    robert_difalco about 10 years
    Do you mean that you want to map response codes to exceptions? In the question I thought you were just asking for the response strings the vendor provided. I'll amend my answer to handle that.
  • Chuck M
    Chuck M about 10 years
    The ClientFilter is the type of solution I was looking for. I am using the new JAX-RS 2.0 client API in Jersey and I don't see the ClientFilter class. There is a ClientResponseFilter instead. I assume that is the class/interface to implement now? jax-rs-spec.java.net/nonav/2.0/apidocs/javax/ws/rs/client/…
  • Janaka Bandara
    Janaka Bandara almost 7 years
    In my case (using Apache CXF with a built-in JAX-RS client) I had to use a ResponseExceptionMapper instead, to map the Response to an appropriate (custom) Exception type, as the custom exception thrown by ClientResponseFilter was not being propagated to the calling thread (CXF's PhaseInterceptorChain was catching and discarding the custom exception on its own, ultimately resulting in the same old JAX-RS exception (InternalServerErrorException in my case) being thrown to the calling thread)
  • Bogdan Calmac
    Bogdan Calmac over 6 years
    Solution 2) here is much less intrusive than creating a filter. The entity returned by the server is indirectly available in the thrown exception (through the Response object).