Error handling in REST API with JAX-RS

2019-04-19 18:23发布

问题:

The task: Instead of receiving general HTTP 500 Internal Server Error in my stacktrace and the same horrible stacktrace on the client side I want to see my customized message with another statuscode (403 for example), that it will be much clearer for the developer, what has happend. And add some message to User about the Exception.

Here are couple of changed classes from my application:

SERVER PART:

AppException.class - all my Server Response exceptions (before giving back to client) I want to transform into this exception. Kinda standard entity class

public class AppException extends WebApplicationException {

Integer status;

/** application specific error code */
int code;

/** link documenting the exception */
String link;

/** detailed error description for developers */
String developerMessage;

public AppException(int status, int code, String message, String developerMessage, String link) {
    super(message);
    this.status = status;
    this.code = code;
    this.developerMessage = developerMessage;
    this.link = link;
}

public int getStatus() {
    return status;
}

public void setStatus(int status) {
    this.status = status;
}

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getDeveloperMessage() {
    return developerMessage;
}

public void setDeveloperMessage(String developerMessage) {
    this.developerMessage = developerMessage;
}

public String getLink() {
    return link;
}

public void setLink(String link) {
    this.link = link;
}

public AppException() {
}

public AppException(String message) {
    super("Something went wrong on the server");
}
}

ÀppExceptionMapper.class - mapps my AppException to the JAX-RS Runtime, instead standard exception, client receives AppException.

    @Provider
public class AppExceptionMapper implements ExceptionMapper<AppException> {

    @Override
    public Response toResponse(AppException exception) {
        return Response.status(403)
                .entity("toResponse entity").type("text/plain").build();
    }


}

ApplicationService.class- my Service class that throws AppException

 @Path("/applications")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface ApplicationService {


    @DELETE
    @Path("/deleteById")
    void deleteById(@NotNull Long id) throws AppException;
}

CLIENT PART:

ErrorHandlingFilter.class- my Response catcher of the AppException. Here I want to transform each Response exception to another exception depending on the status.

@Provider
public class ErrorHandlingFilter implements ClientResponseFilter {

    private static ObjectMapper _MAPPER = new ObjectMapper();

    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
        if (responseContext.getStatus() != Response.Status.OK.getStatusCode()) {
            if(responseContext.hasEntity()) {
                Error error = _MAPPER.readValue(responseContext.getEntityStream(), Error.class);
                String message = error.getMessage();

                Response.Status status = Response.Status.fromStatusCode(responseContext.getStatus());
                AppException clientException;

                switch (status) {

                case INTERNAL_SERVER_ERROR:
                    clientException = new PermissionException(message);
                    break;


                case NOT_FOUND:
                    clientException = new MyNotFoundException(message);
                    break;

                default:
                    clientException =  new WhatEverException(message);
                }
                    throw clientException;
        }
    }
    }
}

PermissionException.class - exception in what I want to transform AppException, if it came with 500 status code.

public class PermissionException extends AppException{

        public PermissionException(String message) {
    super("403 - Forbidden. You dont have enough rights to delete this Application");

}

Integer status;

/** application specific error code */
int code;

/** link documenting the exception */
String link;

/** detailed error description for developers */
String developerMessage;

public PermissionException(int status, int code, String message, String developerMessage, String link) {
    super(message);
    this.status = status;
    this.code = code;
    this.developerMessage = developerMessage;
    this.link = link;
}

public int getStatus() {
    return status;
}

public void setStatus(int status) {
    this.status = status;
}

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getDeveloperMessage() {
    return developerMessage;
}

public void setDeveloperMessage(String developerMessage) {
    this.developerMessage = developerMessage;
}

public String getLink() {
    return link;
}

public void setLink(String link) {
    this.link = link;
}

public PermissionException() {}


}

ApplicationPresenter.class- piece of UI logic, where I want something to do with PermissionException thrown by the ErrorHandlingFilter.

@SpringPresenter
public class ApplicationPresenter implements ApplicationView.Observer {

@Resource
    private ApplicationService applicationService;

    @Resource
    private UiEnvironment uiEnvironment;

@Override
    public void deleteSelectedApplication(BeanItemGrid<Application> applicationGrid) {

        try {
applicationService.deleteById(applicationGrid.getSelectedItem().getId());
                    } catch (PermissionException e) {
                        e.printStackTrace();
                        e.getMessage();
                    } catch (AppException e2) {
                    }
}
}

How can I resolve my problem? I am still receiving standard 500 InternalErrorException.

UPDATED ALMOST THE WHOLE QUESTION ONE MORE TIME!

回答1:

When you have an ExceptionMapper, you don't catch the exception yourself, but have the framework catch it, when the resource method is invoked on an HTTP request.



回答2:

The proper way to perform error handling is by registering ExceptionMapper instances that know what response should be returned in case of specific (or generic) exception.

@Provider
public class PermissionExceptionHandler implements ExceptionMapper<PermissionException>{
    @Override
    public Response toResponse(PermissionException ex){
        //You can place whatever logic you need here
        return Response.status(403).entity(yourMessage).build();
    }  
}

Please take a look at my other answer for more details: https://stackoverflow.com/a/23858695/2588800



回答3:

I have a different approach here. You can try this when starting your jetty server in the main java method

public static void main(String[] args) throws UnknownHostException, JSONException, IOException, Exception {

        MyMain myMain = new MyMain();

        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");

        Server jettyServer = new Server(5550);
        jettyServer.setHandler(context);
        context.setErrorHandler(new ErrorHandler());
        // default error handler for resources out of "context" scope
        jettyServer.addBean(new ErrorHandler());

        ServletHolder jerseyServlet = context.addServlet(org.glassfish.jersey.servlet.ServletContainer.class, "/*");
        jerseyServlet.setInitOrder(0);

        // Tells the Jersey Servlet which REST service/class to load.
        jerseyServlet.setInitParameter("jersey.config.server.provider.classnames",
                ControllerInn.class.getCanonicalName() );

        try {
            jettyServer.start();            
            jettyServer.join();

        } catch (Exception ex) {
            Logger.getLogger(ControllerInn.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            jettyServer.destroy();
        }
    }
    /**
     * Dummy error handler that disables any error pages or jetty related messages and returns our
     * ERROR status JSON with plain HTTP status instead. All original error messages (from our code) are preserved
     * as they are not handled by this code.
     */
    static class ErrorHandler extends ErrorPageErrorHandler {
        @Override
        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
            response.getWriter()
            .append("{\"message\":\"HTTP ERROR ")
            .append(String.valueOf(response.getStatus()))
            .append("\"}");
        }
    }

So you can get an output like this

{"message":"HTTP ERROR 500"}

You can Reference from here



回答4:

This is a Jersey example, but you can extract the needed informations from here. I would only throw an exception and map this exception to any wanted response in the end.

Lets assume you have following ressource method, thowing the exception:

@Path("items/{itemid}/")
public Item getItem(@PathParam("itemid") String itemid) {
  Item i = getItems().get(itemid);
  if (i == null) {
    throw new CustomNotFoundException("Item, " + itemid + ", is not found");
  }

  return i;
}

Create your exception class:

public class CustomNotFoundException extends WebApplicationException {

  /**
  * Create a HTTP 404 (Not Found) exception.
  */
  public CustomNotFoundException() {
    super(Responses.notFound().build());
  }

  /**
  * Create a HTTP 404 (Not Found) exception.
  * @param message the String that is the entity of the 404 response.
  */
  public CustomNotFoundException(String message) {
    super(Response.status(Responses.NOT_FOUND).
    entity(message).type("text/plain").build());
  }
}

Now add your exception mapper:

@Provider
public class EntityNotFoundMapper implements ExceptionMapper<CustomNotFoundException> {
  public Response toResponse(CustomNotFoundException  ex) {
    return Response.status(404).
      entity("Ouchhh, this item leads to following error:" + ex.getMessage()).
      type("text/plain").
      build();
  }
}

In the end you have to register your exception mapper, so it can be used in your application. Here is some pseudo-code:

register(new EntityNotFoundMapper());
//or
register(EntityNotFoundMapper.class);


回答5:

Suggested correctly above, ideal practice is to let the framework catch the Exception for you now that you have implemented an ExceptionMapper. However, one important point overviewing the phenomena which you are executing: if you need to handle any uncaught exceptions, you need to have an Exception class implementing ExceptionMapper which maps to Throwable

public class UncaughtExcep implements ExceptionMapper<Throwable>{

   @Override 
   public Response toResponse(Throwable e){

    }
}

Assuming your class WhatEverException caters to that. If not, then its a good practice to implement