Skip to content

Lab 11: Exception Handling

Description

When a coffee order ID isn't found, we currently throw an Exception. Instead, we'd like to respond with the standard 404 (Not Found) status code.

Goals

  • Learn about ResponseEntity for more control over response headers
  • See different exception handling methods with ExceptionHandler and ControllerAdvice

a. Add Failing Test

Add the following test to the CoffeeOrderWebTest class, which should fail with a NoSuchElementException:

@Test
public void getNonExistentCoffeeOrderReturnsNotFoundStatus() throws Exception {
  mockMvc.perform(get("/api/coffee/orders/9999")
                  .accept(MediaType.APPLICATION_JSON))
         .andExpect(status().isNotFound());
}

b. ResponseEntity.ok

In the CoffeeOrderController, by returning the CoffeeOrderResponse object, which gets converted and written into the body of the HTTP response, but doesn't provide control over the response headers. ResponseEntity provides this control. To use it, change the GET-mapped method to return:

public ResponseEntity<CoffeeOrderResponse> coffeeOrder...

Change the return in the method to use the ResponseEntity.ok() method to return the response:

...
CoffeeOrderResponse response = CoffeeOrderResponse.from(coffeeOrder);
return ResponseEntity.ok(response);

This hasn't fixed the problem yet, but remains compatible with other tests, so those should remain passing (so make sure to run them!).

c. ResponseEntity.notFound

Now you can write code to check for the coffee order being found. If the Optional is present, then return as above, otherwise return "Not Found" like this (note this needs the .build()):

return ResponseEntity.notFound().build();

All the tests should now be passing.

d. Exception Handler

There are some errors that you might want to handle across multiple methods, such as an ID being negative (which might always be invalid). Here, we'll use the @ExceptionHandler which is like a high-level try..catch.

  1. Add a new test to CoffeeOrderWebTest:

    @Test
    public void getNegativeIdReturnsBadRequestStatus() throws Exception {
      mockMvc.perform(get("/api/coffee/orders/-1")
                          .accept(MediaType.APPLICATION_JSON))
             .andExpect(status().isBadRequest());
    }
    

    Run it and watch it fail.

  2. In the CoffeeOrderController add a new method:

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public void handleIllegalArgumentAsBadRequest() {
    }
    

    This will "catch" any IllegalArgumentException thrown within the controller class and will cause an HTTP status of Bad Request (400) to be returned.

    ExceptionHandler Reference

    There are many combinations of ways to handle exceptions and responding with just an HTTP status, or returning a complex object (e.g., an HTTP Problem object). See https://docs.spring.io/spring-framework/docs/5.2.11.RELEASE/spring-framework-reference/web.html#mvc-ann-exceptionhandler for details.

  3. Add code in the GET-mapped method such that if the order ID is less than zero, it throws an IllegalArgumentException.

  4. Run the test, and it should now pass.

    Capture Across Controllers

    If you want to handle exceptions in the same way across multiple controllers, use the @ControllerAdvice annotation. See https://docs.spring.io/spring-framework/docs/5.2.11.RELEASE/spring-framework-reference/web.html#mvc-ann-controller-advice.