221
votes

I'm learning JAX-RS (aka, JSR-311) using Jersey. I've successfuly created a Root Resource and am playing around with parameters:

@Path("/hello")
public class HelloWorldResource {

    @GET
    @Produces("text/html")
    public String get(
        @QueryParam("name") String name,
        @QueryParam("birthDate") Date birthDate) {

         // Return a greeting with the name and age
    }
}

This works great, and handles any format in the current locale which is understood by the Date(String) constructor (like YYYY/mm/dd and mm/dd/YYYY). But if I supply a value which is invalid or not understood, I get a 404 response.

For example:

GET /hello?name=Mark&birthDate=X

404 Not Found

How can I customize this behavior? Maybe a different response code (probably "400 Bad Request")? What about logging an error? Maybe add a description of the problem ("bad date format") in a custom header to aid troubleshooting? Or return a whole Error response with details, along with a 5xx status code?

11

11 Answers

275
votes

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.

70
votes
@Provider
public class BadURIExceptionMapper implements ExceptionMapper<NotFoundException> {

public Response toResponse(NotFoundException exception){

    return Response.status(Response.Status.NOT_FOUND).
    entity(new ErrorResponse(exception.getClass().toString(),
                exception.getMessage()) ).
    build();
}
}

Create above class. This will handle 404 (NotFoundException) and here in toResponse method you can give your custom response. Similarly there are ParamException etc. which you would need to map to provide customized responses.

38
votes

Jersey throws an com.sun.jersey.api.ParamException when it fails to unmarshall the parameters so one solution is to create an ExceptionMapper that handles these types of exceptions:

@Provider
public class ParamExceptionMapper implements ExceptionMapper<ParamException> {
    @Override
    public Response toResponse(ParamException exception) {
        return Response.status(Status.BAD_REQUEST).entity(exception.getParameterName() + " incorrect type").build();
    }
}
27
votes

You could also write a reusable class for QueryParam-annotated variables

public class DateParam {
  private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

  private Calendar date;

  public DateParam(String in) throws WebApplicationException {
    try {
      date = Calendar.getInstance();
      date.setTime(format.parse(in));
    }
    catch (ParseException exception) {
      throw new WebApplicationException(400);
    }
  }
  public Calendar getDate() {
    return date;
  }
  public String format() {
    return format.format(value.getTime());
  }
}

then use it like this:

private @QueryParam("from") DateParam startDateParam;
private @QueryParam("to") DateParam endDateParam;
// ...
startDateParam.getDate();

Although the error handling is trivial in this case (throwing a 400 response), using this class allows you to factor-out parameter handling in general which might include logging etc.

11
votes

One obvious solution: take in a String, convert to Date yourself. That way you can define format you want, catch exceptions and either re-throw or customize error being sent. For parsing, SimpleDateFormat should work fine.

I am sure there are ways to hook handlers for data types too, but perhaps little bit of simple code is all you need in this case.

7
votes

I too like StaxMan would probably implement that QueryParam as a String, then handle the conversion, rethrowing as necessary.

If the locale specific behavior is the desired and expected behavior, you would use the following to return the 400 BAD REQUEST error:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

See the JavaDoc for javax.ws.rs.core.Response.Status for more options.

4
votes

@QueryParam documentation says

" The type T of the annotated parameter, field or property must either:

1) Be a primitive type
2) Have a constructor that accepts a single String argument
3) Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
4) Have a registered implementation of javax.ws.rs.ext.ParamConverterProvider JAX-RS extension SPI that returns a javax.ws.rs.ext.ParamConverter instance capable of a "from string" conversion for the type.
5) Be List, Set or SortedSet, where T satisfies 2, 3 or 4 above. The resulting collection is read-only. "

If you want to control what response goes to user when query parameter in String form can't be converted to your type T, you can throw WebApplicationException. Dropwizard comes with following *Param classes you can use for your needs.

BooleanParam, DateTimeParam, IntParam, LongParam, LocalDateParam, NonEmptyStringParam, UUIDParam. See https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params

If you need Joda DateTime, just use Dropwizard DateTimeParam.

If the above list does not suit your needs, define your own by extending AbstractParam. Override parse method. If you need control over error response body, override error method.

Good article from Coda Hale on this is at http://codahale.com/what-makes-jersey-interesting-parameter-classes/

import io.dropwizard.jersey.params.AbstractParam;

import java.util.Date;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class DateParam extends AbstractParam<Date> {

    public DateParam(String input) {
        super(input);
    }

    @Override
    protected Date parse(String input) throws Exception {
        return new Date(input);
    }

    @Override
    protected Response error(String input, Exception e) {
        // customize response body if you like here by specifying entity
        return Response.status(Status.BAD_REQUEST).build();
    }
}

Date(String arg) constructor is deprecated. I would use Java 8 date classes if you are on Java 8. Otherwise joda date time is recommended.

1
votes

This is the correct behavior actually. Jersey will try to find a handler for your input and will try to construct an object from the provided input. In this case it will try to create a new Date object with the value X provided to the constructor. Since this is an invalid date, by convention Jersey will return 404.

What you can do is rewrite and put birth date as a String, then try to parse and if you don't get what you want, you're free to throw any exception you want by any of the exception mapping mechanisms (there are several).

1
votes

I was facing the same issue.

I wanted to catch all the errors at a central place and transform them.

Following is the code for how I handled it.

Create the following class which implements ExceptionMapper and add @Provider annotation on this class. This will handle all the exceptions.

Override toResponse method and return the Response object populated with customised data.

//ExceptionMapperProvider.java
/**
 * exception thrown by restful endpoints will be caught and transformed here
 * so that client gets a proper error message
 */
@Provider
public class ExceptionMapperProvider implements ExceptionMapper<Throwable> {
    private final ErrorTransformer errorTransformer = new ErrorTransformer();

    public ExceptionMapperProvider() {

    }

    @Override
    public Response toResponse(Throwable throwable) {
        //transforming the error using the custom logic of ErrorTransformer 
        final ServiceError errorResponse = errorTransformer.getErrorResponse(throwable);
        final ResponseBuilder responseBuilder = Response.status(errorResponse.getStatus());

        if (errorResponse.getBody().isPresent()) {
            responseBuilder.type(MediaType.APPLICATION_JSON_TYPE);
            responseBuilder.entity(errorResponse.getBody().get());
        }

        for (Map.Entry<String, String> header : errorResponse.getHeaders().entrySet()) {
            responseBuilder.header(header.getKey(), header.getValue());
        }

        return responseBuilder.build();
    }
}

// ErrorTransformer.java
/**
 * Error transformation logic
 */
public class ErrorTransformer {
    public ServiceError getErrorResponse(Throwable throwable) {
        ServiceError serviceError = new ServiceError();
        //add you logic here
        serviceError.setStatus(getStatus(throwable));
        serviceError.setBody(getBody(throwable));
        serviceError.setHeaders(getHeaders(throwable));

    }
    private String getStatus(Throwable throwable) {
        //your logic
    }
    private Optional<String> getBody(Throwable throwable) {
        //your logic
    }
    private Map<String, String> getHeaders(Throwable throwable) {
        //your logic
    }
}

//ServiceError.java
/**
 * error data holder
 */
public class ServiceError {
    private int status;
    private Map<String, String> headers;
    private Optional<String> body;
    //setters and getters
}
1
votes

Approach 1: By extending WebApplicationException class

Create new exception by extending WebApplicationException

public class RestException extends WebApplicationException {

         private static final long serialVersionUID = 1L;

         public RestException(String message, Status status) {
         super(Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build());
         }
}

Now throw 'RestException' whenever required.

public static Employee getEmployee(int id) {

         Employee emp = employees.get(id);

         if (emp == null) {
                 throw new RestException("Employee with id " + id + " not exist", Status.NOT_FOUND);
         }
         return emp;
}

You can see complete application at this link.

Approach 2: Implement ExceptionMapper

Following mapper handles exception of type 'DataNotFoundException'

@Provider
public class DataNotFoundExceptionMapper implements
        ExceptionMapper<DataNotFoundException> {

    @Override
    public Response toResponse(DataNotFoundException ex) {
        ErrorMessage model = new ErrorMessage(ex.getErrorCode(),
                ex.getMessage());
        return Response.status(Status.NOT_FOUND).entity(model).build();
    }

}

You can see complete application at this link.

0
votes

Just as an extension to @Steven Lavine answer in case you want to open the browser login window. I found it hard to properly return the Response (MDN HTTP Authentication) from the Filter in case that the user wasn't authenticated yet

This helped me to build the Response to force browser login, note the additional modification of the headers. This will set the status code to 401 and set the header that causes the browser to open the username/password dialog.

// The extended Exception class
public class NotLoggedInException extends WebApplicationException {
  public NotLoggedInException(String message) {
    super(Response.status(Response.Status.UNAUTHORIZED)
      .entity(message)
      .type(MediaType.TEXT_PLAIN)
      .header("WWW-Authenticate", "Basic realm=SecuredApp").build()); 
  }
}

// Usage in the Filter
if(headers.get("Authorization") == null) { throw new NotLoggedInException("Not logged in"); }