This documentation is for version 4.5.0.
1. Goal
The goal of the project is to make it easy to have proper error responses for REST APIs built with Spring Boot. Just like Spring Boot itself, it takes an opinionated approach to how the response body should look like in the case of an error.
2. Getting started
2.1. Add the library to your project
The library is available on Maven Central, so it is easy to add the dependency to your project.
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>error-handling-spring-boot-starter</artifactId>
<version>LATEST_VERSION_HERE</version>
</dependency>
compile 'io.github.wimdeblauwe:error-handling-spring-boot-starter:LATEST_VERSION_HERE'
This library is indented to be used with a Spring Boot project. It will not work outside of a Spring Boot project. |
2.2. Usage
2.2.1. Default Exception
handling
By adding the library on the classpath, it will become active.
It registers an @ControllerAdvice
bean in the context that will act if an exception is thrown from a @RestController
method.
Suppose there is a custom Exception like this:
package com.company.application.user;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(UserId userId) {
super("Could not find user with id " + userId);
}
}
When this UserNotFoundException
is thrown from a controller method, the library will return a JSON response like this:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id 123"
}
The HTTP status code will be 500 Internal Server Error.
Next to this quite simple default behaviour, there is special handling for some of the common Spring exceptions and there is an extensive amount of customization options that should make this library a good fit for all of your error handling for REST controllers.
This works for @RestController
classes that are servlet based and for those that use Spring WebFlux (reactive).
2.2.2. Validation exception handling
For some exceptions like MethodArgumentNotValidException
, ConstraintViolationException
,… there is special handling and extra information is returned in the response.
For example, suppose you have this class:
public class ExampleRequestBody {
@Size(min = 10)
private String name;
@NotBlank
private String favoriteMovie;
// getters and setters
}
Which is used as a request body in a controller like this:
@RestController
@RequestMapping("/example")
public class MyExampleController {
@PostMapping
public MyResponse doSomething(@Valid @RequestBody ExampleRequestBody requestBody ) {
// ...
}
}
When sending a JSON request body like this:
{
"name": "",
"favoriteMovie": null
}
The validation fails, and the following JSON response will be returned:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='exampleRequestBody'. Error count: 2",
"fieldErrors": [
{
"code": "INVALID_SIZE",
"property": "name",
"message": "size must be between 10 and 2147483647",
"rejectedValue": "",
"path": "name"
},
{
"code": "REQUIRED_NOT_BLANK",
"property": "favoriteMovie",
"message": "must not be blank",
"rejectedValue": null,
"path": "favoriteMovie"
}
]
}
If there are validation errors on the class level, they will be added in the response as globalErrors .
|
2.2.3. With @SpringBootTest
There is nothing special to configure for unit tests that use @SpringBootTest
.
The library is automatically active when it is on the classpath.
3. Configuration
3.1. HTTP response status
3.1.1. Set HTTP response status via @ResponseStatus
The library uses 500 Internal Server Error as HTTP response code by default, just like Spring Boot does.
To set a specific status code, you can use @ResponseStatus
, which is also standard Spring Boot behaviour.
The library will honor what is set there.
Example:
package com.company.application.user;
@ResponseStatus(HttpStatus.NOT_FOUND) (1)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(UserId userId) {
super("Could not find user with id " + userId);
}
}
1 | Specify the HTTP response status via a class level annotation. |
3.1.2. Set HTTP response status via properties
Use the error.handling.http-statuses
property to set the HTTP response status without adding an annotation to the sources of the Exception class itself.
This is mostly useful if you don’t control the sources of the Exception.
For example:
error.handling.http-statuses.java.lang.IllegalArgumentException=bad_request
By setting this, any IllegalArgumentException
that happens will have a 400 Bad Request response code.
The values are the enum values from org.springframework.http.HttpStatus
.
3.1.3. Use ResponseStatusException
When the exception class is org.springframework.web.server.ResponseStatusException
(or one of the sub-classes), then the response status is taken from the status set on the Exception itself.
As an example, when a MethodNotAllowedException
is thrown, this is the resulting JSON:
{
"code": "METHOD_NOT_ALLOWED",
"message": "405 METHOD_NOT_ALLOWED \"Request method 'OPTIONS' not supported\""
}
and the response code will be 405 Method Not Allowed.
3.1.4. Add HTTP response status in JSON response
It is possible to add the HTTP response status in the JSON response payload itself as well. This is not the case by default. To enable this, set the following property:
error.handling.http-status-in-json-response=true
The resulting JSON response will now be:
{
"status": 404,
"code": "USER_NOT_FOUND",
"message": "Could not find user with id 123"
}
3.2. Error codes
3.2.1. Error code style
By default, the full qualified name of the Exception
class is converted to ALL_CAPS to be used as the code
in the response.
If you like to use the plain full qualified name style for the codes, then specify this property:
error.handling.default-error-code-strategy=FULL_QUALIFIED_NAME
For a class called UserNotFoundException
, this default style (ALL_CAPS
) will result in the following JSON:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id 123"
}
Using FULL_QUALIFIED_NAME
, the result will be:
{
"code": "com.company.application.user.UserNotFoundException",
"message": "Could not find user with id 123"
}
3.2.2. General override of error codes
If the default Error code style is not enough for what you need, you can set a code via the properties by using the full qualified name under the error.handling.codes
key:
error.handling.codes.java.lang.IllegalArgumentException=ILLEGAL_ARGUMENT
Result:
{
"code": "ILLEGAL_ARGUMENT",
"message": "argument was not as expected"
}
This is mostly useful for Exception
types that are not under your own control (E.g. they are coming from a library that you use).
If you do have control, it is probably easier to use Per class override of error code.
3.2.3. Per class override of error code
By adding the @ResponseErrorCode
annotation as a class level annotation, it is possible to define the code
that the response will be using.
Example:
package com.company.application.user;
@ResponseErrorCode("COULD_NOT_FIND_USER")
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(UserId userId) {
super("Could not find user with id " + userId);
}
}
This results in this response:
{
"code": "COULD_NOT_FIND_USER",
"message": "Could not find user with id 123"
}
3.2.4. General override of validation error codes
The library has codes defined for all jakarta.validation.constraints annotations.
It is possible to override those via the application.properties
.
The default code
for @Size
is INVALID_SIZE
, but if you want to change this to SIZE_REQUIREMENT_NOT_MET
, then define the following property:
error.handling.codes.Size=SIZE_REQUIREMENT_NOT_MET
If there is now a validation error for @Size
, then the response body will be:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='exampleRequestBody'. Error count: 1",
"fieldErrors": [
{
"code": "SIZE_REQUIREMENT_NOT_MET", (1)
"property": "name",
"message": "size must be between 10 and 2147483647",
"rejectedValue": "",
"path": "name"
}
]
}
1 | Custom code used for the field error |
3.2.5. Field specific override of validation error codes
It is possible to configure a specific error code that only will be used for a combination of a field with a validation annotation.
Suppose you add a regex to validate password rules:
public class CreateUserRequestBody {
@Pattern(".*{8}")
private String password;
// getters and setters
}
By default, this error is in the response:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='createUserRequestBody'. Error count: 1",
"fieldErrors": [
{
"code": "REGEX_PATTERN_VALIDATION_FAILED",
"property": "password",
"message": "must match \".*{8}\"",
"rejectedValue": "",
"path": "password"
}
]
}
If we would use error.handling.codes.Pattern
for the override, then all @Pattern
annotations in the whole application would use a different code.
If we want to only override this for fields that are named password
, we can use:
error.handling.codes.password.Pattern=PASSWORD_COMPLEXITY_REQUIREMENTS_NOT_MET
This results in:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='createUserRequestBody'. Error count: 1",
"fieldErrors": [
{
"code": "PASSWORD_COMPLEXITY_REQUIREMENTS_NOT_MET",
"property": "password",
"message": "must match \".*{8}\"",
"rejectedValue": "",
"path": "password"
}
]
}
3.3. Error messages
3.3.1. Default behaviour
The library will output the message
property of the Exception into the message
JSON field by default.
For example:
package com.company.application.user;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(UserId userId) {
super("Could not find user with id " + userId);
}
}
The response JSON:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id 123" (1)
}
1 | The output uses the message of the Exception by default. |
3.3.2. General override of error messages
By using error.handling.messages
property, it is possible to globally set an error message for a certain exception or a certain validation annotation.
Exception
Suppose you have this defined:
error.handling.messages.com.company.application.user.UserNotFoundException=The user was not found
The response JSON:
{
"code": "USER_NOT_FOUND",
"message": "The user was not found" (1)
}
1 | The output uses the configured override. |
This can also be used for exception types that are not part of your own application.
For example:
error.handling.messages.jakarta.validation.ConstraintViolationException=There was a validation failure.
Will output the following JSON:
{
"code": "VALIDATION_FAILED",
"message": "There was a validation failure.",
"fieldErrors": [
{
"code": "INVALID_SIZE",
"property": "name",
"message": "size must be between 10 and 2147483647",
"rejectedValue": "",
"path": "name"
}
]
}
Validation annotation
Suppose you have this defined:
error.handling.messages.NotBlank=The property should not be blank
Then the message
in the output is this:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='exampleRequestBody'. Error count: 1",
"fieldErrors": [
{
"code": "REQUIRED_NOT_BLANK",
"property": "name",
"message": "The property should not be blank",(1)
"rejectedValue": "",
"path": "name"
}
]
}
1 | Custom message used for the field error |
So you start with error.handling.messages
and suffix with the name of the validation annotation used (@NotBlank
in the above example).
3.3.3. Field specific override of error messages
It is possible to configure a specific error message that only will be used for a combination of a field with a validation annotation.
Suppose you add a regex to validate password rules:
public class CreateUserRequestBody {
@Pattern(".*{8}")
private String password;
// getters and setters
}
By default, this error is in the response:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='createUserRequestBody'. Error count: 1",
"fieldErrors": [
{
"code": "REGEX_PATTERN_VALIDATION_FAILED",
"property": "password",
"message": "must match \".*{8}\"",
"rejectedValue": "",
"path": "password"
}
]
}
If we would use error.handling.messages.Pattern
for the override, then all @Pattern
annotations in the whole application would use a different message.
If we want to only override this for fields that are named password
, we can use:
error.handling.messages.password.Pattern=The password complexity rules are not met. A password must be 8 characters minimum.
This results in:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='createUserRequestBody'. Error count: 1",
"fieldErrors": [
{
"code": "REGEX_PATTERN_VALIDATION_FAILED",
"property": "password",
"message": "The password complexity rules are not met. A password must be 8 characters minimum.", (1)
"rejectedValue": "",
"path": "password"
}
]
}
1 | Custom error message used in the response |
3.4. Super class hierarchy search
By default, the library will only match HTTP response status, Error codes or Error messages settings in the properties when there is an exact match with the full qualified name of the Exception
.
If you want to define settings for a group of Exceptions that share a common superclass, then this is possible by enabling the error.handling.search-super-class-hierarchy
setting:
error.handling.search-super-class-hierarchy=true
With this in place, we can for instance set the properties for any RuntimeException
sub-class like this:
error.handling.http-statuses.java.lang.RuntimeException=bad_request
error.handling.codes.java.lang.RuntimeException=RUNTIME_EXCEPTION
error.handling.messages.java.lang.RuntimeException=A runtime exception has happened
Assume this exception is thrown:
public class MyException extends RuntimeException {}
Then the response will be:
{
"code": "RUNTIME_EXCEPTION",
"message": "A runtime exception has happened"
}
3.4.1. Reset back to default messaging at a point in exception class hierarchy
Consider that case where you have an exception like:
public class ApplicationException extends RuntimeException {}
but you already have the following configuration:
error.handling.messages.java.lang.RuntimeException=A runtime exception has happened
error.handling.search-super-class-hierarchy=true
You may reset back to default error messaging from a certain point in the class hierarchy (say
my.ApplicationException
) by providing an empty value for that exception.
error.handling.messages.my.ApplicationException=
Now, ApplicationException
and its subclasses will fall back to default messaging behaviour, rather than always showing "A runtime exception has happened".
3.5. Exception handlers
3.5.1. Validation
There are 4 cases that this library will create specific JSON responses when validation errors occur:
-
When using
@Valid
in the parameter of a controller method for a request body. For example:@RestController @RequestMapping("/example") public class MyExampleController { @PostMapping public MyResponse doSomething(@Valid @RequestBody ExampleRequestBody requestBody ) { // ... } }
-
When annotating an
@RequestParam
parameter with extra validation annotations. For example:@RestController @RequestMapping("/example") @Validated public class MyExampleController { @GetMapping public MyResponse doSomething(@NotBlank @RequestParam("param") String param ) { // ... } }
You need to add
org.springframework.validation.annotation.Validated
annotation on the controller class otherwise, the validation annotations on the request parameter will not work. -
When using
@Valid
in the parameter of a controller method for a class that is mapped to request parameters. For example:@RestController @RequestMapping("/example") public class MyExampleController { @GetMapping public MyResponse doSomething(@Valid ExampleRequestParameters requestParameters ) { // ... } }
Where
TestRequestBody
would have standard or custom validation annotations. -
When using validation annotations on Spring components that are themselves annotated with
@Validated
. For example:@Service @Validated public static class TestService { void doSomething(@Valid TestRequestBody requestBody, @NotNull String extraArg) { } }
In all cases, the response JSON will be similar to this:
{
"code": "VALIDATION_FAILED",
"message": "Validation failed for object='exampleRequestBody'. Error count: 4",
"fieldErrors": [
{
"code": "INVALID_SIZE",
"property": "name",
"message": "size must be between 10 and 2147483647",
"rejectedValue": "",
"path": "name"
},
{
"code": "REQUIRED_NOT_BLANK",
"property": "favoriteMovie",
"message": "must not be blank",
"rejectedValue": null,
"path": "favoriteMovie"
}
],
"globalErrors": [
{
"code": "ValidCustomer",
"message": "Invalid customer"
},
{
"code": "ValidCustomer",
"message": "UserAlreadyExists"
}
],
"parameterErrors": [
{
"code": "REQUIRED_NOT_NULL",
"message": "must not be null",
"parameter": "extraArg",
"rejectedValue": null
}
]
}
Breakdown:
-
The
code
is alwaysVALIDATION_FAILED
(unless there was an override defined) -
The
message
indicates what object failed the validation and also indicates the amount of validation errors. -
The
fieldErrors
array contains all field-level validation problems. It shows the name of the property that failed the validation and the value that was received in the request. -
The
parameterErrors
array contains all the parameter-level validation problems. It shows the name of the parameter that failed the validation and the value that was received in the request. -
The
globalErrors
array contains the class-level validation problems.The
code
andmessage
used forglobalErrors
is based on the annotation that was used for validation:@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = CustomerValidator.class) public @interface ValidCustomer { String message() default "Invalid customer"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
As well as the template that is used in the validator itself:
public class CustomerValidator implements ConstraintValidator<ValidCustomer, CreateCustomerFormData> { @Override public boolean isValid(CreateCustomerFormData formData, ConstraintValidatorContext context) { if(...) { context.buildConstraintViolationWithTemplate("UserAlreadyExists").addConstraintViolation(); } } }
If you want to change the message for the global errors, the default Spring mechanism for that keeps working. So use
Now add the translation to the
This results in:
|
3.5.2. Unreadable HTTP message
If a controller receives a message that it cannot read, because the JSON is invalid for example, then a HttpMessageNotReadableException
is thrown.
When this happens, the library will return something like the following response:
{
"code": "MESSAGE_NOT_READABLE",
"message": "JSON parse error: Unexpected character ('i' (code 105)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('i' (code 105)): was expecting double-quote to start field name\n at [Source: (PushbackInputStream); line: 1, column: 3]"
}
3.5.3. Type conversion exceptions
Type conversion exceptions like MethodArgumentTypeMismatchException
and TypeMismatchException
will have some extra info about the class that was expected and the value that was rejected:
{
"code": "ARGUMENT_TYPE_MISMATCH",
"message": "Failed to convert value of type 'java.lang.String' to required type 'com.example.user.UserId'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.PathVariable com.example.user.UserId] for value 'fake_UUID'; nested exception is java.lang.IllegalArgumentException: Invalid UUID string: fake_UUID",
"expectedType": "com.example.user.UserId",
"property": "userId",
"rejectedValue": "fake_UUID"
}
3.5.4. Optimistic locking exceptions
When an org.springframework.orm.ObjectOptimisticLockingFailureException
is thrown, the resulting response will be something like:
{
"code": "OPTIMISTIC_LOCKING_ERROR",
"message": "Object of class [com.example.user.User] with identifier [87518c6b-1ba7-4757-a5d9-46e84c539f43]: optimistic locking failed",
"identifier": "87518c6b-1ba7-4757-a5d9-46e84c539f43",
"persistentClassName": "com.example.user.User"
}
3.5.5. Spring Security exceptions
If Spring Security is on the classpath, then those exceptions will be handled.
They will just have a code
and a message
.
For example:
{
"code": "ACCESS_DENIED",
"message": "Access is denied"
}
The full list of Exception types that are handled:
-
AccessDeniedException
-
AccountExpiredException
-
AuthenticationCredentialsNotFoundException
-
AuthenticationServiceException
-
BadCredentialsException
-
UsernameNotFoundException
-
InsufficientAuthenticationException
-
LockedException
-
DisabledException
3.6. Adding extra properties in the response
It is possible to add extra properties in the JSON response by using the @ErrorResponseProperty
annotation in your custom Exception class.
3.6.1. Via method annotation
This example annotates the getUserId()
method with @ResponseErrorProperty
so that the return value of the method is added to the JSON response:
@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 (1)
public String getUserId() {
return userId.getValue();
}
}
1 | Add the result of this method as an extra property in the response |
The resulting response:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id UserId{id=8c7fb13c-0924-47d4-821a-36f73558c898}",
"userId": "8c7fb13c-0924-47d4-821a-36f73558c898"
}
3.6.2. Via field annotation
This example annotates the userId
field with @ResponseErrorProperty
so that the value of the field is added to the JSON response:
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseErrorCode("USER_NOT_FOUND")
public class UserNotFoundException extends RuntimeException {
@ResponseErrorProperty (1)
private final UserId userId;
public UserNotFoundException(UserId userId) {
super(String.format("Could not find user with id %s", userId));
this.userId = userId;
}
}
1 | Add the result of this method as an extra property in the response |
The resulting response:
{
"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 annotated field can be public or private .
|
3.6.3. Overriding the property name
It is also possible to override the property name that will be used in the response by using the value
argument of the annotation.
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseErrorCode("USER_NOT_FOUND")
public class UserNotFoundException extends RuntimeException {
...
@ResponseErrorProperty("id")
public String getUserId() {
return userId.asString();
}
}
The resulting response:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id UserId{id=8c7fb13c-0924-47d4-821a-36f73558c898}",
"id": "8c7fb13c-0924-47d4-821a-36f73558c898"
}
3.6.4. Null handling
If a property or method that is annotated with @ResponseErrorProperty
returns null
, then the JSON output will not contain the property by default.
If this is desirable, then use the includeIfNull
property on the annotation to change this behaviour:
@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(includeIfNull=true) (1)
public String getUserId() {
return userId.asString();
}
}
1 | Set the includeIfNull setting to true |
The resulting response assuming the passed in userId
is null
:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id UserId{id=8c7fb13c-0924-47d4-821a-36f73558c898}",
"userId": null
}
3.6.5. Global customization of the response JSON
The previous methods can only be used if you have access to the source code of the exception class.
If you want to add some property globally for all exceptions, also those from libraries you import, then you can implement an ApiErrorResponseCustomizer
.
For example, suppose you want to add a timestamp to all error responses. You can do so by declaring the following bean:
@Bean
public ApiErrorResponseCustomizer timestampErrorResponseCustomizer() {
return new ApiErrorResponseCustomizer() {
@Override
public void customize(ApiErrorResponse response) {
response.addErrorProperty("instant", Instant.now());
}
};
}
An example resulting response could look like this:
{
"code": "USER_NOT_FOUND",
"message": "Could not find user with id UserId{id=8c7fb13c-0924-47d4-821a-36f73558c898}",
"instant": "2023-06-14T06:20:12.719357Z"
}
You can declare multiple such beans in your application.
3.7. Custom JSON response field names
If the code
, message
, fieldErrors
and/or globalErrors
field names are not to your liking, then you can customize those through the following properties:
error.handling.json-field-names.code=errorCode
error.handling.json-field-names.message=description
error.handling.json-field-names.field-errors=fieldFailures
error.handling.json-field-names.global-errors=classFailures
With these settings, a response will look similar to this:
{
"errorCode": "VALIDATION_FAILED",
"description": "Validation failed for object='exampleRequestBody'. Error count: 4",
"fieldFailures": [
{
"code": "INVALID_SIZE",
"property": "name",
"message": "size must be between 10 and 2147483647",
"rejectedValue": "",
"path": "name"
}
],
"classFailures": [
{
"code": "ValidCustomer",
"message": "UserAlreadyExists"
}
]
}
3.8. Logging
The library will log a single line to the configured logging output for each Exception that is handled.
This behaviour can be changed to log either nothing at all (NO_LOGGING
), or to log full stack traces (WITH_STACKTRACE
) via the error.handling.exception-logging
property.
If you want to keep logging minimal, but still have a full stacktrace for some exceptions, then you can use the error.handling.full-stacktrace-classes
property like this:
error.handling.exception-logging=MESSAGE_ONLY
error.handling.full-stacktrace-classes[0]=java.lang.NullPointerException
error.handling.full-stacktrace-classes[1]=org.springframework.http.converter.HttpMessageNotReadableException
With this configuration, all exceptions will have a single log line in the logging output, but NullPointerException
and HttpMessageNotReadableException
will have full stack traces printed.
Only the exact matches of the listed classes are used, not the subclasses of the specified classes. |
Another way to have additional logging is to specify a list of HTTP return codes that you want full stack traces for:
error.handling.full-stacktrace-http-statuses[0]=5xx
error.handling.full-stacktrace-http-statuses[1]=403
This configuration will print a full stack trace for all errors in the 5xx range and for the exact 403 error code.
The logging that is done on the response codes is additionally to the logging done on the exception type. You might want to only enable one of the two to avoid double stack traces. |
It is also possible to define on what log level the exception message (and the stacktrace if enabled) should be printed.
By default, everything is printed on ERROR, but using the error.handling.log-levels
property, this can be changed.
error.handling.log-levels.400=DEBUG
error.handling.log-levels.401=INFO
error.handling.log-levels.5xx=ERROR
With this configuration, 400 Bad Request will be printed on DEBUG level. 401 Unauthorized will be printed on INFO. Finally, all status code in the 5xx range will be printed on ERROR.
3.9. Spring Security
3.9.1. AuthenticationEntryPoint
By default, the library will not provide a response when there is an unauthorized exception. It is impossible for this library to provide auto-configuration for this.
There is however a class io.github.wimdeblauwe.errorhandlingspringbootstarter.UnauthorizedEntryPoint
that you can configure in your own security configuration to get the expected behaviour.
First, define the class as a Spring bean. Second, set the bean as the entrypoint.
Example configuration:
public class WebSecurityConfiguration {
@Bean
public UnauthorizedEntryPoint unauthorizedEntryPoint(HttpStatusMapper httpStatusMapper,
ErrorCodeMapper errorCodeMapper,
ErrorMessageMapper errorMessageMapper,
ObjectMapper objectMapper) { (1)
return new UnauthorizedEntryPoint(httpStatusMapper, errorCodeMapper, errorMessageMapper, objectMapper);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
UnauthorizedEntryPoint unauthorizedEntryPoint) throws Exception {
http.httpBasic(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated());
http.exceptionHandling(customizer -> customizer.authenticationEntryPoint(unauthorizedEntryPoint));(2)
return http.build();
}
}
1 | Define the UnauthorizedEntryPoint as a bean. |
2 | Use the bean in the security configuration. |
3.9.2. AccessDeniedHandler
Similar to the AuthenticationEntryPoint, there is also an AccessDeniedHandler
implementation available at io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponseAccessDeniedHandler
.
Example configuration:
public class WebSecurityConfiguration {
@Bean
public AccessDeniedHandler accessDeniedHandler(HttpStatusMapper httpStatusMapper,
ErrorCodeMapper errorCodeMapper,
ErrorMessageMapper errorMessageMapper,
ObjectMapper objectMapper) { (1)
return new ApiErrorResponseAccessDeniedHandler(objectMapper, httpStatusMapper, errorCodeMapper, errorMessageMapper);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
AccessDeniedHandler accessDeniedHandler) throws Exception {
http.httpBasic(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated());
http.exceptionHandling(customizer -> customizer.accessDeniedHandler(accessDeniedHandler));(2)
return http.build();
}
}
1 | Define the AccessDeniedHandler as a bean. |
2 | Use the bean in the security configuration. |
You can perfectly combine the
|
3.10. Handle non-rest controller exceptions
The library is setup in such a way that only exceptions coming from @RestController
classes are handled.
This avoids that the library would interfere with @Controller
method exceptions, if you have a mixed setup with for instance an admin backend using @Controller
for the web interface and an actual API for a mobile app using @RestController
.
The consequence of this is that exceptions that never hit an @RestController
are not processed by the library.
Example of such exceptions:
-
org.springframework.web.HttpRequestMethodNotSupportedException
-
org.springframework.web.HttpMediaTypeNotSupportedException
If you would like to have support for those exceptions (and your project is only using @RestController
classes), then you can define this class:
@ControllerAdvice
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class FallbackExceptionHandler extends ErrorHandlingControllerAdvice {
public FallbackExceptionHandler(List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackHandler,
LoggingService loggingService) {
super(handlers, fallbackHandler, loggingService);
}
}
3.11. Handle filter exceptions
By default, the library will not handle exceptions from custom filters.
Those are implementations of jakarta.servlet.Filter
, usually subclasses of org.springframework.web.filter.OncePerRequestFilter
in a Spring Boot application.
By setting the property error.handling.handle-filter-chain-exceptions
to true
, the library will handle those exceptions and return error responses just like is done for exceptions coming from controller methods.
4. Custom exception handler
If the extensive customization options are not enough, you can write your own ApiExceptionHandler
implementation.
The contract that you need to implement is defined in the io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiExceptionHandler
interface:
package io.github.wimdeblauwe.errorhandlingspringbootstarter;
public interface ApiExceptionHandler {
/**
* Determine if this {@link ApiExceptionHandler} can handle the given {@link Throwable}.
* It is guaranteed that this method is called first, and the {@link #handle(Throwable)} method
* will only be called if this method returns <code>true</code>.
*
* @param exception the Throwable that needs to be handled
* @return true if this handler can handle the Throwable, false otherwise.
*/
boolean canHandle(Throwable exception);
/**
* Handle the given {@link Throwable} and return an {@link ApiErrorResponse} instance
* that will be serialized to JSON and returned from the controller method that has
* thrown the Throwable.
*
* @param exception the Throwable that needs to be handled
* @return the non-null ApiErrorResponse
*/
ApiErrorResponse handle(Throwable exception);
}
There is also the io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.AbstractApiExceptionHandler implementation that you can use as a base class.
|
As an example, imagine you want to add the first-level cause of an Exception.
The implementation could look something like this:
package io.github.wimdeblauwe.errorhandlingspringbootstarter.handler;
@Component (1)
public class CustomExceptionApiExceptionHandler implements ApiExceptionHandler { (2)
@Override
public boolean canHandle(Throwable exception) {
return exception instanceof CustomException; (3)
}
@Override
public ApiErrorResponse handle(Throwable exception) {
CustomException customException = (CustomException) exception;
ApiErrorResponse response = new ApiErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, (4)
"MY_CUSTOM_EXCEPTION",
exception.getMessage());
Throwable cause = customException.getCause();
Map<String, Object> nestedCause = new HashMap<>();
nestedCause.put("code", "CAUSE");
nestedCause.put("message", cause.getMessage());
response.addErrorProperty("cause", nestedCause); (5)
return response;
}
}
1 | Ensure your custom handler is added to the Spring context.
Easiest is just add the @Component annotation. |
2 | Implement the ApiExceptionHandler interface. |
3 | Check the exception type. |
4 | Create the ApiErrorResponse instance. |
5 | Use addErrorProperty to add custom fields to the response. |
This will result in this JSON response:
{
"code": "MY_CUSTOM_EXCEPTION",
"message": "parent exception message",
"cause": {
"code": "CAUSE",
"message": "child IOException message"
}
}
5. Properties
Property | Description | Default |
---|---|---|
|
Allows to enable or disable the error handling |
|
|
Allows to set how the exception should be logged.
One of: |
|
|
Allows to specify a list of full qualified names of Exception classes that will always print a stack trace in the logging, regardless of the |
|
|
Allows to specify a list of HTTP error codes (or ranges) that will always print a stack trace in the logging, regardless of the |
A fixed code like |
|
Allows to specify a map of HTTP error codes (or ranges) to log levels. This allows to specify for each HTTP error code on what log level the message (and stack trace) should be printed. See Logging for more info. |
A fixed code like |
|
Determines how an Exception is converted into a |
|
|
Boolean that allows to add a |
|
|
Allows to set the HttpStatus response code to use for the full qualified name of an |
|
|
Allows to set the code that should be used for the full qualified name of an |
|
|
Allows to set the message that should be used for the full qualified name of an |
|
|
By default, only the exact full qualified name of an |
|
|
The field name that is used to serialize the |
|
|
The field name that is used to serialize the |
|
|
The field name that is used to serialize the |
|
|
The field name that is used to serialize the |
|
|
The field name that is used to serialize the |
|
|
This property allows to remove the |
|
|
Set this to |
|
6. Support
If you have problems with the library, or you are missing a feature, feel free to create an issue on GitHub at github.com/wimdeblauwe/error-handling-spring-boot-starter.