Java Spring HTTP Error Handler

Feb 1st, 2021


Png vectors by Lovepik.com

Springboot Web

Springboot is a power platform for quickly developing enterprise grade web services(REST APIs in our case).

In a previous installment, we talked about using Springboot request interceptors and request scope in order to add some central logging and traceability.

In this installment, we will talk about catching errors that may occur in the lifetime of a request, and handling how the client will see the error.

Controller Advice

The @ControllerAdvice annotation is a specialization of the @Component annotation. The @ControllerAdvice Spring component provides every @Controller class with a central exception handler.

Extending the ResponseEntityExceptionHandler class with our controller advice component gives the ability to define handler methods with the @ExceptionHandler annotation, and control how different types of exceptions will be handled.

Handling HTTP Errors with @ControllerAdvice

Custom Exceptions and Status Code Mapping

A common pattern for mapping responses to errors, and one we will use in this installment, is to throw custom exceptions, that will tell our @ControllerAdvice component how to respond to that particular error.

Lets look at a common case, the 404 Not Found error. This typically occurs when a client navigates to a resource that doesn't exist.


public class ResourceNotFoundException extends RuntimeException {

  public static final int STATUS_CODE = 404;

  private String message;

  public ResourceNotFoundException(String message){
    this.message = message;
    super(message);
  }

  //getters
  //setters
}

We embedded the 404 HTTP status code as a constant. Since we extend the RuntimeException class, we inherit the message property, and call the parent classes constructor with the super keyword.

Book API

Now we need an actual API and endpoint to call. We will use what seems to be a classic example on BuildBench, the book API.

The Datastore

We will use the Spring Data JPA for this post, you can read more about it here. We will also use the Lombok Plugin to generate getters, setters and constructors.

The first thing we need, is a Book entity, this will represent a table in an SQL datastore.


@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "books")
public class Book {

  @Id
  private UUID id;

  @Column(unique = true)
  private String name;

  private String author;

  @Column(unique = true)
  private String isbn;

}

Our book entity will have a UUID primary key, that we will manually assign to each one. Also, both the name and isbn fields must be unique.

Next we will need a book repository


@Repository
public interface BookRepository extends JpaRepository<UUID, Book> {
  
  Book findByName(String name);

  Book findByIsbn(String isbn);

  List<Book> findByAuthor(String author);

  @Query("select b from Book b")
  List<Book> getAll();

}

The API Components

Now we need bean to represent a create book request


@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreateBookRequest {

  private String name;

  private String author;

  private String isbn;

}

Finally, a controller


@RestController
public class BookController {

  private final BookRepository bookRepository;

  @Autowired
  public BookController(BookRepository bookRepository){
    this.bookRepository = bookRepository;
  }

  
  @GetMapping("/books/all")
  public List<Book> getAllBooks(){
    return this.bookRepository.getAll();
  }  

  @GetMapping("/books/{isbn}")
  public Book getBookByIsbn(@PathParam(name = "uuid") String isbn){
    return this.bookRepository.findByIsbn(isbn); 
  } 

  @PostMapping("/books")
  public Book submitBook(@RequestBody CreateBookRequest createBookRequest){
    return this.bookRepository.save(new Book(UUID.randomUuid(), createBookRequest.getName(), createBookRequest.getAuthor(), createBookRequest.getIsbn());
  }
}

So, we can submit a new book, list all books and also get books by isbn.

The Exception Handler

Back to the original problem, we want a central place to catch errors generated by HTTP requests, and control the response sent back to the client.

Now we will create the controller advice


@ControllerAdvice
public class HttpErrorHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(value = { Exception.class })
  protected ResponseEntity<Object> handleGenericException(RuntimeException ex, WebRequest request) {
    return handleExceptionInternal(ex, ex.getMessage(), new HttpHeaders(), HttpStatus.CONFLICT, request);
  }

  @ExceptionHandler(value = {ResourceNotFoundException.class})
  protected ResponseEntity<Object> handleMyException(ResourceNotFoundException ex, WebRequest request) {
    return handleExceptionInternal(ex, ex.getMessage(), new HttpHeaders(), HttpStatus.valueOf(ex.STATUS_CODE), request);
  }
}

We have created two methods, one is blanket statement, to catch all exceptions not specified and returns 500 Internal Server Error Response, the other catches all instances of ResourceNotFoundException and returns 404 Not Found status.

@ExceptionHandler

The @ExceptionHandler annotation tells the @ControllerAdvice method that it should intercept all exceptions of the type specified in the value parameter of the annotation.

ResponseEntity

The ResponseEntity return type allows us to return any response body that we would like.

ResponseEntityExceptionHandler

As mentioned earlier, we extend the ResponseEntityExceptionHandler class, from which we inherit the handleExceptionInternal method.

The handleExceptionInternal method allows us to pass the response body, HTTP headers and status code of our choosing back to the client that made the request.

Generating an Error

Now that we have everything in place, lets generate an error when a client tries to fetch a book with an isbn that does not exist in our database.


  @GetMapping("/books/{isbn}")
  public Book getBookByIsbn(@PathParam(name = "isbn") String isbn){
    Book result = this.bookRepository.findByIsbn(isbn);
    if(book == null){
      throw new ResourceNotFoundException("Book does not exists in database");
    }
    return book;
  } 

If the requestor request a book that does not exist, the controller will throw an ResourceNotFoundException, and the API will return a 404 status, with {message: "Book does not exists in database"} as a response body.

Conclusion

In a previous installment, we have gone over using Springboot HTTP interceptors to handle information about individual HTTP requests.

In this installment, we covered using Spring ControllerAdvice to centrally handle exceptions generated by requests. Patterns similar to this give us total control over the response body, status code and HTTP headers returned to a client when an error occurs.

Comments




Navagation