Every shop has bugs nobody has found yet. When a route throws an exception, Spring answers 500 Internal Server Error with an HTML page, and the shop's page, which expects JSON, can't read it. In this shop the Gift card (product 8) has no category, so .toString() is called on null and throws a NullPointerException.
You replace that page with an exception handler: a method in the controller marked @ExceptionHandler. Spring calls it whenever a route of that controller throws a matching exception, and sends what it returns. Here is one for another kind of exception, answering 400:
@ExceptionHandler(IllegalArgumentException.class)
Object badRequest(IllegalArgumentException error) {
return ResponseEntity.status(400).body(Map.of("error", "Bad request"));
}
A 500 answer should never carry the exception's own message: it names your classes, tables and files, gives an attacker a map of your code, and means nothing to a customer. Send a short message instead, and write the details to the log with log.error, where only you read them. Once your handler catches an exception, Spring doesn't log it anymore, so your line is the only trace. Spring can pass the handler the exception and the HttpServletRequest: request.getMethod(), request.getRequestURI() and error.getMessage() give the method, the path and the error's message.
The check sends three requests, closes the database for a fourth, and runs your test on the shop and on two broken copies. It must print:
The shop gets three requests:
GET /products/1 -> 200 {"category": "Kitchen", "name": "Mug"}
GET /products/99 -> 404 {"error": "Product not found"}
GET /products/8 -> 500 {"error": "Something went wrong on our side"}
log: ERROR GET /products/8 failed: Cannot invoke "Object.toString()" because the return value of "java.util.Map.get(Object)" is null
The database closes:
GET /products/1 -> 500 {"error": "Something went wrong on our side"}
log: ERROR GET /products/1 failed: Failed to obtain JDBC Connection
Your tests on the shop:
testCrashAnswersJson ... ok
1 test: OK
Your tests on a copy with Spring's HTML error page:
testCrashAnswersJson ... ERROR
UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html;charset=utf-8]
1 test: 1 failed, so they caught this bug
Your tests on a copy that sends the error to the visitor:
testCrashAnswersJson ... FAIL
AssertionError: expected: <{error=Something went wrong on our side}> but was: <{error=Cannot invoke "Object.toString()" because the return value of "java.util.Map.get(Object)" is null}>
1 test: 1 failed, so they caught this bug
Your task: in ShopController, below getProduct, write an exception handler for Exception.class: Object serverError(Exception error, HttpServletRequest request). It logs one line with log.error and the text "{} {} failed: {}", filled with the method, the path and the error's message, and answers {"error": "Something went wrong on our side"} with the status 500. Then, in ErrorTests, write testCrashAnswersJson: get /products/8 as a Map and check both the status code and the body.
Assignment
Follow the Coding Tutorial to add the exception handler and its log line, then a test that checks both the status and the JSON body of a crash.
Hint
The handler works for every route of the controller, so it sits on its own below getProduct, not inside it. A test that checks only the status code lets the copy that leaks the error through, because that is a 500 too: compare response.getBody() with Map.of("error", "Something went wrong on our side") as well.
Free plan: 30 tutor messages. PRO is unlimited.
