9

I'd like to return 404 when the response object is null for every response automatically in spring boot.

I need suggestions.

I don't want to check object in controller that it is null or not.

Tugrul
  • 1,760
  • 4
  • 24
  • 39
  • 1
    You can read this topic : [spring boot exception handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc) – fiskra May 10 '17 at 09:12
  • I don't want to check object in controller if its nul or not ? – Tugrul May 10 '17 at 09:22

4 Answers4

7

You need more than one Spring module to accomplish this. The basic steps are:

  1. Declare an exception class that can be used to throw an exception when a repository method does not return an expected value.
  2. Add a @ControllerAdvice that catches the custom exception and translates it into an HTTP 404 status code.
  3. Add an AOP advice that intercepts return values of repository methods and raises the custom exception when it finds the values not matching expectations.

Step 1: Exception class

public class ResourceNotFoundException extends RuntimeException {}

Step 2: Controller advice

@ControllerAdvice
public class ResourceNotFoundExceptionHandler
{
  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public void handleResourceNotFound() {}
}

Step 3: AspectJ advice

@Aspect
@Component
public class InvalidRepositoryReturnValueAspect
{
  @AfterReturning(pointcut = "execution(* org.example.data.*Repository+.findOne(..))", returning = "result")
  public void intercept(final Object result)
  {
    if (result == null)
    {
      throw new ResourceNotFoundException();
    }
  }
}

A sample application is available on Github to demonstrate all of this in action. Use a REST client like Postman for Google Chrome to add some records. Then, attempting to fetch an existing record by its identifier will return the record correctly but attempting to fetch one by a non-existent identifier will return 404.

manish
  • 19,695
  • 5
  • 67
  • 91
  • Intercepting repo returns is not a good idea I thing. Is it possible to intercept controller returns ? – Tugrul May 10 '17 at 12:07
  • If you use your own controllers for the REST API endpoints, sure, just change the pointcut to point to controller methods instead of repository methods. – manish May 10 '17 at 16:04
  • You can use `ResponseStatusException(HttpStatus.NOT_FOUND, "not found")` instead of custom exception + custom controller advice btw. `org.springframework.web.server.ResponseStatusException` – O_Prime Sep 12 '22 at 09:26
4

Simplest way to do this in Spring is write your own exception class like below

@ResponseStatus(value = HttpStatus.NOT_FOUND)
class ResourceNotFoundException extends RuntimeException{
}

Then just throw the ResourceNotFoundException from anywhere.

if (something == null) throw new ResourceNotFoundException();

For more -> Read

Amit Phaltankar
  • 3,341
  • 2
  • 20
  • 37
  • I don't want to check object in controller if its nul or not ? I want to return 404 for all null responses for every request. – Tugrul May 10 '17 at 10:24
  • As i mentioned you can throw the exception from anywhere...from service or Dao – Amit Phaltankar May 10 '17 at 10:33
  • 1
    Tanks Amit, but I don't want to check any logic to send exception or sth. Just I want to return to client 404 if return object is null. I want a layer to manupulate response just before returning phase to client. – Tugrul May 10 '17 at 10:36
  • Then I will suggest you to go through Controller Advice. Hopefully this will help you https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#using-controlleradvice-classes – Amit Phaltankar May 10 '17 at 10:46
3

There is no need to throw exceptions, now ResponseBodyAdvice does the trick:

@ControllerAdvice
public class NullTo404 implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
            ServerHttpResponse response) {
        if (body == null) {
            response.setStatusCode(HttpStatus.NOT_FOUND);
        }
        return body;
    }   
}

Similarly, you can implement ResponseBodyAdvice<Optional<?>>, and check for Optional.isEmpty() before setting the response status. It has the added benefit of working nicely with CrudRepository. Most controller methods eventually ends like this:

public Optional<Product> getProductBySku(@PathVariable String sku) {
    // logic goes here...
    return productRepository.findBySku(sku);
}
zsellera
  • 61
  • 3
2

Similar to @manish's answer (https://stackoverflow.com/a/43891952/986160) but without the AspectJ pointcut and using another @ControllerAdvice instead:

Step 1: NotFoundException class:

public class NotFoundException extends RuntimeException {
    public NotFoundException(String msg) {
        super(msg);
    }
    public NotFoundException() {}
}

Step 2: Check if body returned in endpoint is null and throw NotFoundException:

@ControllerAdvice
public class NotFoundAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body == null) {
            throw new NotFoundException("resource not found");
        }
        return body;
    }
}

Step 3: handle NotFoundException and make the response have a status of 404

@ControllerAdvice
public class GlobalExceptionAdvice {

    @Data
    public class ErrorDetails {
        private Date timestamp;
        private String message;
        private String details;

        public ErrorDetails(Date timestamp, String message, String details) {
            super();
            this.timestamp = timestamp;
            this.message = message;
            this.details = details;
        }
    }

    @ExceptionHandler(NotFoundException.class)
    public final ResponseEntity<ErrorDetails> notFoundHandler(Exception ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
                request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }
}

Alternative to Step 3:

You can just annotate your NotFoundException with @ResponseStatus and override fillInStackTrace() (from https://stackoverflow.com/a/31263942/986160) so that it has similar effect to GlobalExceptionAdvice and doesn't show stacktrace like this:

@ResponseStatus(value = HttpStatus.NOT_FOUND,reason =  "resource not found")
public class NotFoundException extends RuntimeException {
    public NotFoundException(String msg) {
        super(msg);
    }
    public NotFoundException() {}

    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;
    }
}
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106