Add a body to a 404 Not Found Exception

28,496

Basic Idea


First option is to define error objects and return them as 404 Not Found body. Something like following:

Map<String, String> errors = ....;
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errors);

Instead of returning a typical ResponseEntity, you can throw an Exception that will be resolved to a 404 Not Found. Suppose you have a NotFoundException like:

@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {}

Then if you throw this exception in your controllers, you would see something like:

{  
   "timestamp":1461621047967,
   "status":404,
   "error":"Not Found",
   "exception":"NotFoundException",
   "message":"No message available",
   "path":"/greet"
}

If you want to customize the message and other parts of body, you should define a ExceptionHandler for NotFoundException.

Introducing Exception Hierarchies


If you're creating a RESTful API and want to have different Error Codes and Error Messages for different exceptional cases, you can create a hierarchy of exceptions representing those cases and extract message and code from each one.

For example, you can introduce an exception, say, APIException which is super-class of all other exceptions thrown by your controllers. This class defines a code/message pair like:

public class APIException extends RuntimeException {
    private final int code;
    private final String message;

    APIException(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int code() {
        return code;
    }

    public String message() {
        return message;
    }
}

Each subclass depending on the nature of its exception can provide some sensible values for this pair. For example, we could have an InvalidStateException:

@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public class InvalidStateException extends APIException {
    public InvalidStateException() {
        super(1, "Application is in invalid state");
    }
}

Or that notorious not found ones:

@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class SomethingNotFoundException extends APIException {
    public SomethingNotFoundException() {
        super(2, "Couldn't find something!");
    }
}

Then we should define an ErrorController that catches those exceptions and turn them to meaningful JSON representations. That error controller may look like following:

@RestController
public class APIExceptionHandler extends AbstractErrorController {
    private static final String ERROR_PATH = "/error";
    private final ErrorAttributes errorAttributes;

    @Autowired
    public APIExceptionHandler(ErrorAttributes errorAttributes) {
        super(errorAttributes);
        this.errorAttributes = errorAttributes;
    }

    @RequestMapping(path = ERROR_PATH)
    public ResponseEntity<?> handleError(HttpServletRequest request) {
        HttpStatus status = getStatus(request);

        Map<String, Object> errors = getErrorAttributes(request, false);
        getApiException(request).ifPresent(apiError -> {
            errors.put("message" , apiError.message());
            errors.put("code", apiError.code());
        });
        // If you don't want to expose exception!
        errors.remove("exception");


        return ResponseEntity.status(status).body(errors);
    }

    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }

    private Optional<APIException> getApiException(HttpServletRequest request) {
        RequestAttributes attributes = new ServletRequestAttributes(request);
        Throwable throwable = errorAttributes.getError(attributes);
        if (throwable instanceof APIException) {
            APIException exception = (APIException) throwable;
            return Optional.of(exception);
        }

        return Optional.empty();
    }
}

So, if you throw an SomethingNotFoundException, the returned JSON would be like:

{  
   "timestamp":1461621047967,
   "status":404,
   "error":"Not Found",
   "message":"Couldn't find something!",
   "code": 2,
   "path":"/greet"
}
Share:
28,496
luso
Author by

luso

Just learning

Updated on July 09, 2022

Comments

  • luso
    luso almost 2 years

    In an REST API generated with JHipster, I want to throw some 404 exceptions. It is normally done with

    return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    

    which actualy results in a 404 response to the xhr request. The problem is that in the front side, JHipster parses the response with

    angular.fromJson(result)
    

    and such result is empty when the 404 is the actual response, which makes the parse to fail.

    If I point to an unmapped URI, lets say /api/user while my controller maps to /api/users (note the plural) the 404 I got from the API has a body in it:

    {
        "timestamp": "2016-04-25T18:33:19.947+0000",
        "status": 404,
        "error": "Not Found",
        "message": "No message available",
        "path": "/api/user/myuser/contact"
    }
    

    which is correctly parse in angular.

    How can I create a body like this? Is this exception thrown by spring or is tomcat who throws it?

    I tried this: Trigger 404 in Spring-MVC controller? but I cant set the parameters of the response.

  • luso
    luso about 8 years
    Thank you Ali, but this way I can't define the message. I tried adding the reason property as in @ResponseStatus(value = HttpStatus.NOT_FOUND, reason="Resource not found") but then the response is something like {"message":"error.404","description":"Resource not found","fieldErrors":null}
  • luso
    luso about 8 years
    By the way I throw it this way return Optional.ofNullable(entity) .map(result -> new ResponseEntity<>( result, HttpStatus.OK)) .orElseThrow(HTTPNotFoundException::new);
  • xersiee
    xersiee over 6 years
    Do you know if I can easily log all exceptions that are mapped using @ResponseStatus?
  • Albert Lázaro de Lara
    Albert Lázaro de Lara over 5 years
    This code doesn't work: errorAttributes.getError expected WebRequest not RequestAttributes
  • Admin
    Admin over 2 years
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.