简体   繁体   中英

Spring @ExceptionHandler returns wrong HttpStatus code

TL;DR: @ExceptionHandler function is returning 200 OK instead of 400 Bad Request for a MissingServletParameterException when calling HttpServletResponse.getStatus & HttpStatus.valueOf(HttpServletResponse.getStatus)).name() . MissingServletParameterException is only used as an example, also happens for other exceptions too.

Hello,

The issue I'm having is that I'm trying to integrate Raygun (a crash reporting system) with our Java/Spring Boot application. The easiest way I've found was to create a custom exception handler that would display the error message to the user as well as pass the exception to Raygun.

Originally, I tried the implementation suggested here with my own Raygun implementation added https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

@ControllerAdvice
class GlobalDefaultExceptionHandler {
  public static final String DEFAULT_ERROR_VIEW = "error";

  private static ApiAccessToken accessToken = new ApiAccessToken();
  private static String databaseName = null;

  @ExceptionHandler(value = Exception.class)
  public ModelAndView
  defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
    // If the exception is annotated with @ResponseStatus rethrow it and let
    // the framework handle it
    if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
        throw e;
     }

    // Otherwise setup and send the user to a default error-view.
    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", e);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName(DEFAULT_ERROR_VIEW);

    // Display the error message to the user, and send the exception to Raygun along with any user details provided.
    RaygunClient client = new RaygunClient("<MyRaygunAPIKey>");

    if (accessToken.getUsername() != null && accessToken.getDatabaseName() != null) {
        ArrayList tags = new ArrayList<String>();
        tags.add("username: " + accessToken.getUsername());
        tags.add("database: " + accessToken.getDatabaseName());
        client.Send(e, tags);
        accessToken = null;
        return mav;

    } else if (databaseName != null) {
        ArrayList tags = new ArrayList<String>();
        tags.add("database: " + databaseName);
        client.Send(e, tags);
        databaseName = null;
        return mav;

    } else {
        client.Send(e);
        return mav;
    }
}

The problem I encountered with this is that we have both public and private API endpoints. The private API endpoints are used for our iOS applications, whereas the public API endpoints have no front-end. They were designed for businesses to be able to integrate into their own systems (PowerBI, Postman, custom integrations, etc). And so there is no views that I can redirect to using ModelAndView.

Instead, what I've decided to do is instead of using ModelAndView, I'm just returning a string that has been formatted to mimic Spring's default JSON error message.

@ExceptionHandler(value = Exception.class)
public @ResponseBody String defaultErrorHandler(HttpServletRequest req, HttpServletResponse resp, Exception e) throws Exception {

    // Create a customised error message that imitates the Spring default Json error message
    StringBuilder sb = new StringBuilder("{ \n")
            .append("    \"timestamp\": ").append("\"").append(DateTime.now().toString()).append("\" \n")
            .append("    \"status\": ").append(resp.getStatus()).append(" \n")
            .append("    \"error\": ").append("\"").append(HttpStatus.valueOf(resp.getStatus()).name()).append("\" \n")
            .append("    \"exception\": ").append("\"").append(e.getClass().toString().substring(6)).append("\" \n")
            .append("    \"message\": ").append("\"").append(e.getMessage()).append("\" \n")
            .append("    \"path\": ").append("\"").append(req.getServletPath()).append("\" \n")
            .append("}");

    String errorMessage = String.format(sb.toString());

    // Display the error message to the user, and send the exception to Raygun along with any user details provided.
    RaygunClient client = new RaygunClient("<MyRaygunAPIKey>");

    if (accessToken.getUsername() != null && accessToken.getDatabaseName() != null) {
        ArrayList tags = new ArrayList<String>();
        tags.add("username: " + accessToken.getUsername());
        tags.add("database: " + accessToken.getDatabaseName());
        client.Send(e, tags);
        accessToken = null;
        return errorMessage;

    } else if (databaseName != null) {
        ArrayList tags = new ArrayList<String>();
        tags.add("database: " + databaseName);
        client.Send(e, tags);
        databaseName = null;
        return errorMessage;

    } else {
        client.Send(e);
        return errorMessage;
    }
}

The only issue with this is that when I purposefully cause an exception to be thrown, it returns with a HTTP status of 200 OK which is obviously not correct.

For instance, this is with defaultErrorHandler() commented out (sends nothing to Raygun):

{
"timestamp": "2017-07-18T02:59:45.131+0000",
"status": 400,
"error": "Bad Request",
"exception": 
"org.springframework.web.bind.MissingServletRequestParameterException",
"message": "Required String parameter ‘foo’ is not present",
"path": "/api/foo/bar/v1"
}

And this is with it not commented out (sends the exception to Raygun):

{ 
"timestamp": "2017-07-25T06:21:53.895Z" 
"status": 200 
"error": "OK" 
"exception": "org.springframework.web.bind.MissingServletRequestParameterException" 
"message": "Required String parameter 'foo' is not present" 
"path": "/api/foo/bar/V1" 
}

Any help or advice on what I'm doing wrong would be greatly appreciated. Thank you for your time.

In your controller advice try this way to map exception type to Http-Status as follows:

if (ex instanceof MyException)
{//just an example.
    return new ResponseEntity<>(e, HttpStatus.BAD_REQUEST);
}
else
{//all other unhandled exceptions
    return new ResponseEntity<>(e, HttpStatus.INTERNAL_SERVER_ERROR);
}

Here MyException is the type of exception you are throwing at runtime. Say I am handling Bad request.

I'm still unsure why it was returning a 200 OK status when an exception was being thrown. But I've realised that what I was doing with trying to create a string that mimics Spring's default json error message, was overly complex and not necessary at all.

Once I had sent the exception through to Raygun, I can just rethrow the exception and let the framework handle it like any exception annotated with @ResponseStatus .

@ExceptionHandler(value = Exception.class)
public void defaultErrorHandler(Exception e) throws Exception {

    RaygunClient client = new RaygunClient("<MyRaygunAPIKey>");

    // If the exception is annotated with @ResponseStatus rethrow it and let
    // the framework handle it
    if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
        throw e;
    }

    // Otherwise send the exception Raygun and then rethrow it and let the framework handle it
    if (accessToken.getUsername() != null && accessToken.getDatabaseName() != null) {
        ArrayList tags = new ArrayList<String>();
        tags.add("username: " + accessToken.getUsername());
        tags.add("database: " + accessToken.getDatabaseName());
        client.Send(e, tags);
        accessToken = null;
        throw e;

    } else if (databaseName != null) {
        ArrayList tags = new ArrayList<String>();
        tags.add("database: " + databaseName);
        client.Send(e, tags);
        databaseName = null;
        throw e;

    } else {
        client.Send(e);
        throw e;
    }
}

A bare bones implentation would look like this:

@ExceptionHandler(value = Exception.class)
public void defaultErrorHandler(Exception e) throws Exception {

    RaygunClient client = new RaygunClient("<MyRaygunAPIKey>");

    // If the exception is annotated with @ResponseStatus rethrow it and let the framework handle it
    if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
        throw e;
    }
    // Otherwise send the exception Raygun and then rethrow it and let the framework handle it
    else {
        client.Send(e);
        throw e;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM