@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
}
I created the error-handling-spring-boot-starter library for Spring Boot because I was unhappy with the default response that Spring Boot 2 provides out-of-the-box.
With Spring Boot 3, there is now support for ProblemDetail and we can have a look on how that changes things.
If you first want to learn more about using ProblemDetail, I would suggest to first read Spring Boot 3 : Error Responses using Problem Details for HTTP APIs by sivalabs. If you like video more, have a look to Spring 6 and Problem Details on YouTube.
In summary, this is how ProblemDetail works:
You need to opt-in to enable it for framework errors by setting spring.webmvc.problemdetails.enabled=true
(or spring.webflux.problemdetails.enabled
if you use WebFlux).
You can also enable it by creating an exception handler that extends from ResponseEntityExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
}
For your own exceptions, you need to write a custom ExceptionHandler
that converts the exception to an instance of ProblemDetail
. For example:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(BookmarkNotFoundException.class)
ProblemDetail handleBookmarkNotFoundException(BookmarkNotFoundException e) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
problemDetail.setTitle("Bookmark Not Found");
problemDetail.setType(URI.create("https://api.bookmarks.com/errors/not-found"));
return problemDetail;
}
}
By doing this, you get a response like this:
{
"type": "https://api.bookmarks.com/errors/not-found",
"title": "Bookmark Not Found",
"status": 404,
"detail": "Bookmark with id: 111 not found",
"instance": "/api/bookmarks/111"
}
If you don’t want to write an ExceptionHandler
, you can have your exception classes extend from org.springframework.web.ErrorResponseException
which forces you to create a ProblemDetail
instance in the constructor of your custom exception.
Check out the linked blog post for example.
You can add custom fields in the error response by using the properties
Map of ProblemDetail
.
As a reminder, this is how a typical error response looks like with this library:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id UserId{id=8c7fb13c-0924-47d4-821a-36f73558c898}",
"userId": "8c7fb13c-0924-47d4-821a-36f73558c898"
}
The only thing you need to have this, is an exception like this:
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseErrorCode("USER_NOT_FOUND")
public class UserNotFoundException extends RuntimeException {
private final UserId userId;
public UserNotFoundException(UserId userId) {
super(String.format("Could not find user with id %s", userId));
this.userId = userId;
}
@ResponseErrorProperty
public String getUserId() {
return userId.getValue();
}
}
See Better Error Handling for Your Spring Boot REST APIs for a more detailed introduction to the library.
When we compare how error-handling-spring-boot-starter works to using ProblemDetail, we can notice the following:
With error-handling-spring-boot-starter, you can just add the dependency on your classpath. It becomes enabled right away.
It works for any Exception
, there is no need to extend from the Spring framework exception type ErrorResponseException
and to manually write code to create a ProblemDetail
inside each of those exceptions.
By annotating your custom exception, you can add fields to the response. For example,
just annotate a getter on your custom exception with @ResponseErrorProperty
and the response of that getter will be automatically added to the error response.
The error-handling-spring-boot-starter has a rich configuration model to be able to influence the code
and the message
that is used for exceptions.
Both those from your own code and exceptions from other libraries.
All by just adding some properties to your application.properties
.
There is no need to write a custom exception handler. Everything is driven from the exceptions you throw.
ProblemDetail is an implementation of RFC 7807. The response used by error-handling-spring-boot-starter is not backed by any standard. One of the things this standard dictates is:
Consumers MUST use the "type" string as the primary identifier for the problem type.
With type
a field that should contain an URI reference.
I find the use of the code
field in responses of error-handling-spring-boot-starter
a bit easier to work with.
They are also automatically derived from the name of the Exception.
I really wonder how much projects will actually put a valid URI in there. I also wonder if people will use different URI’s depending on if the application is running in production or not for example.
Given all of the above, I think that error-handling-spring-boot-starter
still provides a lot of value, also for Spring Boot 3 and Spring 6.
To use it with Spring Boot 3, be sure to use version 4.0.0.
This post showed an overview of the error-handling-spring-boot-starter
library and how it compares to ProblemDetail in Spring 6.
If you have any questions or remarks, feel free to post a comment at GitHub discussions.