Part of
problem4j
package of libraries.
Spring Web integration module for problem4j-core
. library that integrates the RFC Problem Details
model with exception handling in Spring Boot.
The desired usage of this library is to make all your custom exceptions extend ProblemException
from problem4j-core
.
It's still possible to create custom @RestControllerAdvice
-s, but some nuances with @Order
are necessary (explained
in Usage chapter, which covers also how response bodies for build-in Spring exceptions are overwritten).
- âś… Automatic mapping of exceptions to responses with
Problem
objects compliant with RFC 7807. - âś… Mapping of exceptions extending
ProblemException
to responses withProblem
objects. - âś… Fallback mapping of
Exception
toProblem
objects representing500 Internal Server Error
. - âś… Simple configuration thanks to Spring Boot autoconfiguration.
import io.github.malczuuu.problem4j.core.Problem;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
@Order(Ordered.LOWEST_PRECEDENCE - 20)
@Component
@RestControllerAdvice
public class ExampleExceptionAdvice {
@ExceptionHandler(ExampleException.class)
public ResponseEntity<Problem> method(ExampleException ex, WebRequest request) {
Problem problem =
Problem.builder()
.type("http://example.com/errors/example-error")
.title("Example Title")
.status(400)
.detail(ex.getMessage())
.instance("https://example.com/instances/example-instance")
.build();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON);
HttpStatus status = HttpStatus.valueOf(problem.getStatus());
return new ResponseEntity<>(problem, headers, status);
}
}
Add library as dependency to Maven or Gradle. See the actual versions on Maven Central. Add it along with repository in your dependency manager. Java 17 or higher is required to use this library.
Tested with Spring Boot 3+
, but mostly on 3.5.x
. However, the idea for v1.x
of this library was to be backwards
compatible down to 3.0.0
. Integration with Spring Boot 4 (once its released) will most likely be released as v2.x
if
v1.x
won't be compatible.
- Maven:
<dependencies> <dependency> <groupId>io.github.malczuuu.problem4j</groupId> <artifactId>problem4j-spring-web</artifactId> <version>${problem4j-spring-web.version}</version> </dependency> </dependencies>
- Gradle (Groovy or Kotlin DSL):
dependencies { implementation("io.github.malczuuu.problem4j:problem4j-spring-web:${problem4j-spring-web.version}") }
Overriding of build-in exceptions is performed by custom ExceptionMapping
and its implementations.
These mappings are instantiated in ExceptionMappingConfiguration
with
@ConditionalOnClass
, per appropriate exception. Therefore, if using this library with previous versions, mappings for
exception classes that are not present in classpath are silently ignored.
While creating your own @RestControllerAdvice
, make sure to position it with right @Order
. In order for your custom
implementation to work seamlessly, make sure to position it on at least Ordered.LOWEST_PRECEDENCE - 1
(the lower
the value, the higher the priority), as ExceptionAdvice
covers the most generic Exception
class.
@RestControllerAdvice |
covered exceptions | @Order(...) |
---|---|---|
ProblemEnhancedExceptionHandler |
Spring's internal exceptions | Ordered.LOWEST_PRECEDENCE - 10 |
ProblemExceptionAdvice |
ProblemException |
Ordered.LOWEST_PRECEDENCE - 10 |
ConstraintViolationExceptionAdvice |
ConstraintViolationException |
Ordered.LOWEST_PRECEDENCE - 10 |
ExceptionAdvice |
Exception |
Ordered.LOWEST_PRECEDENCE |
Library can be configured with following properties.
Property that specifies how exception handling imported with this module should print "detail"
field of Problem
model (lowercase
, capitalized
- default, uppercase
). Useful for keeping the same style of errors coming from
library and your application.
This module overrides Spring MVC's default (often minimal or plain-text) responses for many framework exceptions and
produces structured RFC 7807 Problem
objects. ExceptionMappingConfiguration
registers these mappings.
These error mappings to disallow leaking too much information to the client application. It more information is
necessary, feel free to override specific ExceptionMapping
, register it as @Serivce
,
@Component
or @Bean
and exclude specific nestedExceptionMappingConfiguration
configuration class.
Overriding whole ProblemEnhancedExceptionHandler
is not recommended, although such necessities are sometimes
understandable.
What triggers it: An async request (e.g. DeferredResult
, Callable
, WebAsync) exceeded configured timeout before
producing a value.
Mapping: AsyncRequestTimeoutMapping
Example:
{
"status": 500,
"title": "Internal Server Error"
}
What triggers it: Spring's type conversion system could not convert a controller method return value or property to the required type (fatal conversion issue, not just a simple mismatch).
Mapping: ConversionNotSupportedMapping
Example:
{
"status": 500,
"title": "Internal Server Error"
}
What triggers it: Explicitly thrown ErrorResponseException
(or subclasses like ResponseStatusException
). Each of
these exceptions carry HTTP status within it as well as details to be used in application/problem+json
response.
Mapping: ErrorResponseMapping
Example:
{
"type": "https://example.org/problem-type",
"title": "Some Error",
"status": 400,
"detail": "Explanation of the error",
"instance": "https://example.org/instances/123",
"extraKey": "extraValue"
}
What triggers it: Client Accept
header doesn't match any producible media type from controller.
Mapping: HttpMediaTypeNotAcceptableMapping
{
"status": 406,
"title": "Not Acceptable"
}
What triggers it: Request Content-Type
not supported by any HttpMessageConverter
for the target endpoint.
Mapping: HttpMediaTypeNotSupportedMapping
{
"status": 415,
"title": "Unsupported Media Type"
}
What triggers it: Incoming request body couldn't be parsed/deserialized (malformed JSON, wrong structure, EOF, etc.).
Mapping: HttpMessageNotReadableMapping
{
"status": 400,
"title": "Bad Request"
}
What triggers it: Server failed to serialize the response body (e.g. Jackson serialization problem) after controller returned a value.
Mapping: HttpMessageNotWritableMapping
{
"status": 500,
"title": "Internal Server Error"
}
What triggers it: HTTP method not supported for a particular endpoint (e.g. POST
to an endpoint allowing only GET
).
Mapping: HttpRequestMethodNotSupportedMapping
{
"status": 405,
"title": "Method Not Allowed"
}
What triggers it: Multipart upload exceeds configured max file or request size.
Mapping: MaxUploadSizeExceededMapping
{
"status": 413,
"title": "Content Too Large",
"detail": "Max upload size exceeded",
"max": 1048576
}
What triggers it: Bean Validation (JSR 380) failed for a @Valid
annotated argument (e.g. request body DTO) during data
binding.
Mapping: MethodArgumentNotValidMapping
{
"status": 400,
"title": "Bad Request",
"detail": "Validation failed",
"errors": [
{
"field": "email",
"error": "must be a well-formed email address"
},
{
"field": "age",
"error": "must be greater than or equal to 18"
}
]
}
Field names convention may be formatted (e.g. snake_case
) by configuring spring.jackson.property-naming-strategy
.
What triggers it: A required URI template variable was not provided (e.g. handler expected {id}
path variable that was
absent in request mapping resolution).
Mapping: MissingPathVariableMapping
{
"status": 400,
"title": "Bad Request",
"detail": "Missing path variable",
"name": "id"
}
What triggers it: Required query parameter is missing (e.g. @RequestParam(required=true)
not supplied by client).
Mapping: MissingServletRequestParameterMapping
{
"status": 400,
"title": "Bad Request",
"detail": "Missing request param",
"param": "q",
"kind": "string"
}
What triggers it: Required multipart request part missing (e.g. file field in a multipart/form-data POST not provided).
Mapping: MissingServletRequestPartMapping
{
"status": 400,
"title": "Bad Request",
"detail": "Missing request part",
"param": "file"
}
What triggers it: DispatcherServlet could not find any handler (no matching controller) for the request (requires
throwExceptionIfNoHandlerFound=true
).
Mapping: NoHandlerFoundMapping
{
"status": 404,
"title": "Not Found"
}
What triggers it: Static resource handling (e.g. ResourceHttpRequestHandler
) couldn't resolve the requested resource (
Spring Boot 3.x when resource chain handling is enabled).
Mapping: NoResourceFoundMapping
{
"status": 404,
"title": "Not Found"
}
What triggers it: General binding issues with request parameters, headers, path variables (e.g. missing header required
by @RequestHeader
).
Mapping: ServletRequestBindingMapping
{
"status": 400,
"title": "Bad Request"
}
What triggers it: Failed to bind a web request parameter/path variable to a controller argument due to type mismatch (
e.g. age=abc
where age
expects an integer).
Mapping: TypeMismatchMapping
{
"status": 400,
"title": "Bad Request",
"detail": "Type mismatch",
"property": "age",
"kind": "integer"
}
What triggers it: Any unhandled exception flowing through ResponseEntityExceptionHandler
without a dedicated mapping.
Result example:
{
"status": 500,
"title": "Internal Server Error"
}
problem4j
- Documentation repository.problem4j-core
- Core library definingProblem
model andProblemException
.problem4j-jackson
- Jackson module for serializing and deserializingProblem
objects.problem4j-spring-web
- Spring Web module extendingResponseEntityExceptionHandler
for handling exceptions and returningProblem
responses.