Jax-Rs/Jersey How to Customize Error Handling

JAX-RS / Jersey how to customize error handling?

There are several approaches to customize the error handling behavior with JAX-RS. Here are three of the easier ways.

The first approach is to create an Exception class that extends WebApplicationException.

Example:

public class NotAuthorizedException extends WebApplicationException {
public NotAuthorizedException(String message) {
super(Response.status(Response.Status.UNAUTHORIZED)
.entity(message).type(MediaType.TEXT_PLAIN).build());
}
}

And to throw this newly create Exception you simply:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
// An unauthorized user tries to enter
throw new NotAuthorizedException("You Don't Have Permission");
}

Notice, you don't need to declare the exception in a throws clause because WebApplicationException is a runtime Exception. This will return a 401 response to the client.

The second and easier approach is to simply construct an instance of the WebApplicationException directly in your code. This approach works as long as you don't have to implement your own application Exceptions.

Example:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
// An unauthorized user tries to enter
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

This code too returns a 401 to the client.

Of course, this is just a simple example. You can make the Exception much more complex if necessary, and you can generate what ever http response code you need to.

One other approach is to wrap an existing Exception, perhaps an ObjectNotFoundException with an small wrapper class that implements the ExceptionMapper interface annotated with a @Provider annotation. This tells the JAX-RS runtime, that if the wrapped Exception is raised, return the response code defined in the ExceptionMapper.

Custom Jersey Error Handling, how to catch response at client side?

Is there a way to get the errorMessage on the client side (in the catch block) without using Response as a return type?

You can use response.readEntity(new GenericType<List<Country>>(){}). This way you will still have access to the Response. Though in this case there will be no stacktrace. There is only an exception on the client when you try to use get(ParsedType). The reason is that with get(ParsedType), there is no other way to handle the error status. But using Response, the developer should do the checking against the status. So the method could look more like

public List<Country> findAll() throws ClientErrorException {
WebTarget resource = webTarget;
resource = resource.path("countries");
Response response = resource.request(javax.ws.rs.core.MediaType.APPLICATION_XML).get();
if (response.getStatus() != 200) {
System.out.println(response.getHeaderString("errorResponse"));
return null;
} else {
return response.readEntity(new GenericType<List<Country>>(){});
}
}

Though instead of in the header, I would just send the message out as the response body. That's just me

super(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity( message).type(MediaType.TEXT_PLAIN).build());

Client side

if (response.getStatus() != 200) {
System.out.println(response.readEntity(String.class));
return null;
}

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

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.

Handling custom error response in JAX-RS 2.0 client library

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.

How to customize error response when deserializing failed

The Jackson provider already has ExceptionMappers1 that will just return the exception message when an exception is caught. If you want, you can just create your own ExceptionMapper for these exceptions, i.e. JsonParseException and JsonMappingException, and just return the message you want. Then just register those mappers with the Jersey application. This should override the ones registered by Jackson.

The reason your exception mapper doesn't work is because the ones from Jackson are more specific. The more specific one always gets invoked.

1 - namely JsonParseExceptionMapper and JsonMappingExceptionMapper

Jersey - content negotiation for error handling

Maybe just a workaround, but ...

You could try to use a ContainerResponseFilter, get all accpeted MediaTypes from the ContainerRequestContext and limit the response MediaType by reset the entity with ContainerResponseContext.setEntity.


There might be better solutions!

Used jersey 2.12:

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.List;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;

import path.to.MyException;

@Provider
public class EntityResponseFilter implements ContainerResponseFilter {

@Override
public void filter( ContainerRequestContext reqc , ContainerResponseContext resc ) throws IOException {
List<MediaType> mediaTypes = reqc.getAcceptableMediaTypes();
MediaType mediaType = new MediaType("application", "vnd-mycompany-error");
if( mediaTypes.contains( mediaType) && resc.getEntity() instanceof MyDao /* or MyDto, or null, or whatever */) {
resc.setEntity( resc.getEntity(), new Annotation[0], mediaType );
}
// ...
}
}

Hope this was helpfull somehow:)

How to Handle Invalid DataType Error in Jersey JAX-RS

You can handle this by implementing error mapper. For the example you shown above, it looks like Exception is getting thrown at internal processing while mapping JSON Data to POJO.


If you look at logs closely, you will find errors like InvalidFormatException or JsonMappingException while processing data.

You can create Exception Mapper for the error you are getting. I would recommend to use super JsonMappingException as it will take care of error like invalid type, invalid JSON in request payload, etc:

@Provider
public class GenericExceptionMapper extends Throwable implements ExceptionMapper<JsonMappingException> {
@Override
public Response toResponse(JsonMappingException thrExe) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("errorMessage", "Invalid input provided");
return Response.status(400).entity(jsonObject.toString())
.type("application/json").build();
}
}


Related Topics



Leave a reply



Submit