HTTP Error codes based on Exception in Spring Boot
In our previous installment, We have made sure our application follows a hypermedia format called HAL. However, At most of the controller methods, we have been throwing RuntimeException. These by design would cause 500 Error codes. But REST principles dictate that proper message or indication must be given back to the client when possible.
GET http://localhost:8080/carts/4
HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 12 Nov 2020 23:01:24 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"timestamp": "2020-11-12T23:01:24.911+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/carts/4"
}
Code language: HTTP (http)
For example, if a resource was not available, then a 404 not found would be an appropriate response. Let’s try to achieve this behavior. I will take CartController
for this example. Let’s try to respond with a 404 if a cart with a given id does not exist.
Here is attempt number 1 using the spring-MVC’s ResponseEntity wrapper.
@GetMapping("/{cartId}")
ResponseEntity<EntityModel<Cart>> get(@PathVariable Integer cartId) {
Optional<Cart> byId = cartRepository.findById(cartId);
if (!byId.isPresent()) {
return ResponseEntity.notFound().build();
}
EntityModel<Cart> entityModel = EntityModel.of(byId.get(),
linkTo(methodOn(CartController.class).get(cartId)).withSelfRel());
return ResponseEntity.ok().body(entityModel);
}
Code language: Java (java)
Even though this would work, There are a couple of problems with this approach. Even before this change, the response code was already for records that exist. This is additional code debt. Also, we can’t keep checking the records for every possible scenario.
Here is an attempt with a cleaner approach. First, create a new Exception for scenarios where resources don’t exist. Make sure this exception is extending RuntimeException. Annotate this Exception with @ResponseStatus
.
package com.springhow.examples.springboot.rest.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}
Code language: Java (java)
Here, This @ResponseStatus
bind annotation is part of SpringMVC and it contains an already existing auto-configuration that will take care of the Response’s status code. Now throw this exception wherever you want to have 404
response code if the resource doesn’t exist. So the attempt 1 can be re-written as
@GetMapping("/{cartId}")
EntityModel<Cart> get(@PathVariable Integer cartId) {
Cart cart = cartRepository.findById(cartId).orElseThrow(ResourceNotFoundException::new);
return EntityModel.of(cart,
linkTo(methodOn(CartController.class).get(cartId)).withSelfRel());
}
Code language: Java (java)
let’s try calling this API for an invalid Cart id.
GET http://localhost:8080/carts/4
HTTP/1.1 404
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 12 Nov 2020 23:05:33 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"timestamp": "2020-11-12T23:05:33.911+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/carts/4"
}
Code language: HTTP (http)
Notice how the response HTTP code, as well as the status error message, has changed. In the same regard, Some of the responses may need positive varients of HTTP response codes. For example, a DELETE method’s appropriate response code would be 204 No Content
. And a create method would return 201 Created
or 202 Accepted
as a response. As these are success cases, there are no exceptions to use. But there is also an easier way to do this. Just annotate the method with @ResponseStatus
as shown below.
@PostMapping("/")
@ResponseStatus(HttpStatus.CREATED) EntityModel<Cart> create(@RequestBody Cart cart) {
cart.setStatus(CartStatus.NEW);
Cart created = cartRepository.save(cart);
return EntityModel.of(created,
linkTo(methodOn(CartController.class).get(created.getId())).withSelfRel());
}
Code language: Java (java)
Summary
To conclude, We are at the end of this series and I would like to recollect what we have learned. At the beginning of this series, we learned about Restful services and their basics. Once we knew that we were on the right track we made sure the service follows all RESTful principles by implementing hateoas and appropriate messages and error codes.