1. Context
This is a Java library for working with RFC 9457 standardized Belgif problems.
With this library, RFC 9457 Problems can be automagically treated as standard java exceptions:
-
the server side can throw
io.github.belgif.rest.problem.api.Problem
exceptions (or subclasses), which are transparently converted to RFC 9457 compliant "application/problem+json" responses -
the client side can catch
io.github.belgif.rest.problem.api.Problem
exceptions (or subclasses), which are transparently thrown when an RFC 9457 compliant "application/problem+json" response is received
The library consists of these modules:
-
belgif-rest-problem: library for RFC 9457 standardized Belgif problems
-
belgif-rest-problem-java-ee: Java EE / Jakarta EE integration for belgif-rest-problem
-
belgif-rest-problem-spring-boot-2: Spring Boot 2.x integration for belgif-rest-problem
-
belgif-rest-problem-spring-boot-3: Spring Boot 3.x integration for belgif-rest-problem
-
belgif-rest-problem-validator: validation library that generates Belgif-compliant BadRequestProblem with InputValidationIssues
2. Release notes
2.2. Version 0.11
belgif-rest-problem:
-
Add in(Input<?>) to InputValidationIssue for a fluent single input setter
belgif-rest-problem-spring:
-
Fix non-deterministic configuration for retrieving parameter names from Spring MVC annotations
2.3. Version 0.10
belgif-rest-problem:
-
Remove @ApplicationException annotation on Problem, because it could potentially cause compilation errors when used in combination with annotation processors
-
Disallow creation of InputValidationIssue with inputs[] of size 1
Potentially breaking:
-
removed
InputValidationIssue.setInputs(List<Input<?>> inputs)
-
removed
InputValidationIssue.setInputs(Input<?>… inputs)
-
removed
InputValidationIssue.input(Input<?> input)
-
belgif-rest-problem-java-ee:
-
Add EJBExceptionMapper that unwraps Problem cause, to address the removed @ApplicationException annotation
belgif-rest-problem-spring:
-
Added support for InvalidRequestException thrown by the Atlassian swagger-request-validator
2.4. Version 0.9
belgif-rest-problem-bom:
-
Added Maven BOM (Bill of Materials) for dependency versions of belgif-rest-problem modules
belgif-rest-problem:
-
Add WWW-Authenticate HTTP response header to token-related problem types
belgif-rest-problem-java-ee:
-
Ensure ProblemClientResponseFilter gets registered for JAX-RS clients
2.5. Version 0.8
belgif-rest-problem-spring:
-
Map HttpRequestMethodNotSupportedException to HTTP 405 "Method Not Allowed" + Allow HTTP header
-
Map HttpMediaTypeNotAcceptableException to HTTP 406 "Not Acceptable"
-
Map HttpMediaTypeNotSupportedException to HTTP 415 "Unsupported Media Type"
-
Sanitize BadRequestProblem detail message for HttpMessageNotReadableException
2.6. Version 0.7
belgif-rest-problem-validator:
-
Make RequestValidator extensible by introducing AbstractRequestValidator base class
belgif-rest-problem-spring:
-
Extract ProblemWebClientCustomizer to belgif-rest-problem-spring-boot-2 and belgif-rest-problem-spring-boot-3 to fix NoSuchMethodError compatibility issue
-
Add AnnotationParameterNameDiscoverer to retrieve parameter names from Spring MVC annotations for bean validation
belgif-rest-problem-java-ee:
-
Add JaxRsParameterNameProvider to retrieve parameter names from JAX-RS annotations for bean validation
2.7. Older versions
See Release Notes.
3. Getting started
3.1. Dependencies
3.1.1. Spring Boot
<dependency>
<groupId>io.github.belgif.rest.problem</groupId>
<artifactId>belgif-rest-problem-spring-boot-3</artifactId>
<version>latest</version>
</dependency>
compile 'io.github.belgif.rest.problem:belgif-rest-problem-spring-boot-3:latest'
<dependency>
<groupId>io.github.belgif.rest.problem</groupId>
<artifactId>belgif-rest-problem-spring-boot-2</artifactId>
<version>latest</version>
</dependency>
compile 'io.github.belgif.rest.problem:belgif-rest-problem-spring-boot-2:latest'
3.1.2. Jakarta EE
<dependency>
<groupId>io.github.belgif.rest.problem</groupId>
<artifactId>belgif-rest-problem-java-ee</artifactId>
<version>latest</version>
<classifier>jakarta</classifier>
</dependency>
compile 'io.github.belgif.rest.problem:belgif-rest-problem-java-ee:latest:jakarta'
<dependency>
<groupId>io.github.belgif.rest.problem</groupId>
<artifactId>belgif-rest-problem-java-ee</artifactId>
<version>latest</version>
</dependency>
compile 'io.github.belgif.rest.problem:belgif-rest-problem-java-ee:latest'
3.2. Usage
At the core of this library lies an exception hierarchy of Belgif-standardized problem types, with the possibility to define your own custom problem types. Integration with Spring Boot and Jakarta EE works out of the box.
3.2.1. Returning a problem from a REST API
When implementing a REST API, returning a problem is as simple as throwing the problem exception from your application code:
throw new BadRequestProblem(
new InputValidationIssue(InEnum.PATH, "sectorCode", sectorCode)
.title("Invalid sector code"));
3.2.2. Catching problems when calling a REST API
When calling a REST API, handling a problem is as simple as catching the problem exception in your application code:
try {
api.someOperation();
} catch (BadRequestProblem p) {
String detail = p.getDetail();
List<InputValidationIssue> issues = p.getIssues();
// handle the BadRequestProblem
} catch (ResourceNotFoundProblem p) {
String detail = p.getDetail();
List<InputValidationIssue> issues = p.getIssues();
// handle the ResourceNotFoundProblem
} catch (Problem e) {
// handle all other problems
}
If the REST API returns a problem type for which you do not have a corresponding @ProblemType on your classpath, a DefaultProblem is thrown instead as fallback.
See @ProblemType and ProblemTypeRegistry for more details.
|
3.3. Spring Boot
As of version 6.0, Spring Framework also contains basic integration for RFC 9475 problem types. |
3.3.1. RestController
Problem exceptions and framework exceptions thrown by RestController components are automatically converted to a proper RFC 9457 application/problem+json
response.
Bean validation
Exceptions thrown by the Java Bean Validation Framework are automatically converted to a proper RFC 9457 application/problem+json
response.
When using a Spring MVC annotation without name (e.g. @RequestParam String name instead of @RequestParam("name") String name ), the parameter name for Bean Validation errors is retrieved via reflection as fallback.
For this to work, your application code must be compiled with the -parameters flag (see maven-compiler-plugin).
Otherwise, you will see parameter names like arg0 .
|
Atlassian swagger-request-validator
InvalidRequestExceptions thrown by the Atlassian swagger-request-validator are automatically converted to a proper RFC 9457 application/problem+json
response.
-
For Spring Boot 3, use swagger-request-validator-spring-webmvc.
-
For Spring Boot 2, use swagger-request-validator-springmvc.
On Spring Boot 2, validation.request.path.missing
should be set to level IGNORE in order to return a 404 error for a missing path. See documentation of the Atlassian library for more information.
3.3.2. RestTemplate
Problem support is automatically enabled when constructing your RestTemplate through an autowired RestTemplateBuilder.
When manually constructing a RestTemplate instance, you’ll need to apply the ProblemRestTemplateCustomizer yourself.
3.3.3. Reactive WebClient
Problem support is automatically enabled when constructing your WebClient through an autowired WebClient.Builder.
When manually constructing a WebClient instance, you’ll need to apply the ProblemWebClientCustomizer yourself.
3.3.4. RestClient
Problem support is automatically enabled when constructing your RestClient through an autowired RestClient.Builder.
When manually constructing a RestClient instance, you’ll need to apply the ProblemRestClientCustomizer yourself.
3.4. Jakarta EE
While this library may work on other application servers, it has only been thoroughly tested on JBoss EAP 7.4 (with MicroProfile XP 4.0) and WildFly 31. Furthermore, the following dependencies are expected to be "provided" by the application server. If not, you’ll need to add these dependencies to your application:
|
3.4.1. JAX-RS service
Problem exceptions and framework exceptions (including bean validation) thrown by JAX-RS service components are automatically converted to a proper RFC 9457 application/problem+json
response.
3.4.2. JAX-RS client
Problem support is automatically enabled for @Inject
-ed javax.ws.rs.client.Client(Builder)
.
When manually constructing a JAX-RS client instance, you’ll need to wrap it yourself with client = ProblemSupport.enable(client)
.
JAX-RS async and reactive client API are not fully supported yet. |
3.4.3. MicroProfile REST Client
Problem support is automatically enabled for MicroProfile REST Client.
3.4.4. RESTEasy Proxy Framework
Problem support must be manually enabled for RESTEasy Proxy Framework by wrapping the proxy with client = ProblemSupport.enable(client)
.
3.5. Code generators
Code generators should be configured to use the belgif-rest-problem types for problem-related model classes, instead of generating them.
3.5.1. openapi-generator-maven-plugin
When using openapi-generator-maven-plugin, configure <schemaMappings>
as follows:
<schemaMappings>
Problem=io.github.belgif.rest.problem.api.Problem,
InputValidationProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
InputValidationIssue=io.github.belgif.rest.problem.api.InputValidationIssue,
InvalidParamProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
InvalidParam=io.github.belgif.rest.problem.api.InvalidParam
</schemaMappings>
3.5.2. swagger-codegen-maven-plugin
When using swagger-codegen-maven-plugin, configure <importMappings>
as follows:
<importMappings>
Problem=io.github.belgif.rest.problem.api.Problem,
InputValidationProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
InputValidationIssue=io.github.belgif.rest.problem.api.InputValidationIssue,
InvalidParamProblem=io.github.belgif.rest.problem.api.InputValidationProblem,
InvalidParam=io.github.belgif.rest.problem.api.InvalidParam
</importMappings>
4. Problems
The Belgif REST Guidelines define a standard error handling approach, based on RFC 9457 "Problem Details for HTTP APIs".
The belgif-rest-problem
library implements these standardized problem types as an exception hierarchy, with an abstract
io.github.belgif.rest.problem.api.Problem
base class (containing the standardized "type", "href", "title", "status", "detail" and "instance" properties).
Problem ├── ClientProblem (4xx) | └── InputValidationProblem 1..* InputValidationIssue ├── ServerProblem (5xx) └── DefaultProblem (fallback)
4.1. Belgif problem types
The package io.github.belgif.rest.problem
contains all problem types defined in the Belgif REST Guidelines.
Problem ├── ClientProblem (4xx) | ├── InputValidationProblem 1..* InputValidationIssue | | ├── BadRequestProblem (400) | | ├── MissingPermissionProblem (403) | | └── ResourceNotFoundProblem (404) | ├── ExpiredAccessTokenProblem (401) | ├── InvalidAccessTokenProblem (401) | ├── NoAccessTokenProblem (401) | ├── MissingScopeProblem (403) | ├── PayloadTooLargeProblem (413) | ├── TooManyRequestsProblem (429) | └── TooManyFailedRequestsProblem (429) └── ServerProblem (5xx) ├── InternalServerErrorProblem (500) ├── BadGatewayProblem (502) └── ServiceUnavailableProblem (503)
|
4.2. Custom problem types
Besides the standardized Belgif problem types, custom problem types can also be defined.
The naming convention should be respected for custom problem type URIs:
-
urn:problem-type:<org>:<type>
for organization-wide problem types -
urn:problem-type:<org>:<api>:<type>
for API-local problem types
@ProblemType(TooManyResultsProblem.TYPE)
public static class TooManyResultsProblem extends ClientProblem {
public static final String TYPE = "urn:problem-type:cbss:legallog:tooManyResults";
public static final URI TYPE_URI = URI.create(TYPE);
public static final URI HREF =
URI.create("https://api.ksz-bcss.fgov.be/legallog/v1/refData/problemTypes/" + TYPE);
public static final String TITLE = "Too Many Results";
public static final int STATUS = 400;
private static final long serialVersionUID = 1L;
public TooManyResultsProblem() {
super(TYPE_URI, HREF, TITLE, STATUS);
setDetail("Result's number of global transactions exceeds the limit");
}
public TooManyResultsProblem(int limit) {
this();
setDetail("Result's number of global transactions exceeds the limit of " + limit);
}
}
Your custom problem type can define additional properties if necessary.
For problem types related to input parameters, you can extend from InputValidationProblem
instead of Problem
.
Don’t forget to add the @ProblemType annotation.
|
On Jakarta EE containers, custom problem types are discovered automagically through CDI.
On Spring Boot, only package io.github.belgif.rest.problem
is scanned for @ProblemType annotations.
Additional packages to scan can be configured in your application.properties:
io.github.belgif.rest.problem.scan-additional-problem-packages=com.acme.custom
5. belgif-rest-problem-validator
The io.github.belgif.rest.problem.validation.RequestValidator
can be used to perform validations on the input parameters of an API request.
This validation does not stop on the first invalid input.
It performs all configured validations and if any of them failed, a BadRequestProblem
is thrown, containing each encountered InputValidationIssue
.
// example for an API resource with query parameters:
// ssin, enterpriseNumber, startDate, endDate and password
new RequestValidator()
.ssin(Input.query("ssin", ssin)) (1)
.enterpriseNumber(Input.query("enterpriseNumber", enterpriseNumber)) (2)
.period(Input.query("startDate", startDate), Input.query("endDate", endDate)) (3)
.exactlyOneOf(Input.query("ssin", ssin),
Input.query("enterpriseNumber", enterpriseNumber)) (4)
.custom(() -> { (5)
// custom validation logic returning Optional<InputValidationIssue>
if (!"secret".equals(password)) {
return Optional.of(new InputValidationIssue(InEnum.QUERY, "password", password)
.type("urn:problem-type:cbss:input-validation:example:invalidPassword")
.title("Invalid password"));
}
return Optional.empty();
})
.validate(); (6)
1 | validate SSIN structure |
2 | validate enterprise number structure |
3 | validate endDate >= startDate |
4 | cross-parameter validation: either ssin or enterprise number should be present, but not both |
5 | custom validations are also supported |
6 | perform the configured validations, throws BadRequestProblem if any validations failed |
Example BadRequestProblem for invalid request /resource?ssin=00000000000&enterpriseNumber=1111111111&startDate=2023-12-31&endDate=2023-01-01&password=oops
{
"type": "urn:problem-type:belgif:badRequest",
"href": "https://www.belgif.be/specification/rest/api-guide/problems/badRequest.html",
"title": "Bad Request",
"status": 400,
"detail": "The input message is incorrect",
"issues": [
{
"type": "urn:problem-type:cbss:input-validation:invalidStructure",
"title": "Input value has invalid structure",
"detail": "SSIN 11111111111 is invalid",
"in": "query",
"name": "ssin",
"value": "11111111111"
},
{
"type": "urn:problem-type:cbss:input-validation:invalidStructure",
"title": "Input value has invalid structure",
"detail": "Enterprise number 2222222222 is invalid",
"in": "query",
"name": "enterpriseNumber",
"value": "2222222222"
},
{
"type": "urn:problem-type:cbss:input-validation:invalidPeriod",
"title": "Period is invalid",
"detail": "endDate should not preceed startDate",
"inputs": [
{
"in": "query",
"name": "startDate",
"value": "2023-12-31"
},
{
"in": "query",
"name": "endDate",
"value": "2023-01-01"
}
]
},
{
"type": "urn:problem-type:cbss:input-validation:exactlyOneOfExpected",
"title": "Exactly one of these inputs must be present",
"detail": "Exactly one of these inputs must be present: ssin, enterpriseNumber",
"inputs": [
{
"in": "query",
"name": "ssin",
"value": "11111111111"
},
{
"in": "query",
"name": "enterpriseNumber",
"value": "2222222222"
}
]
},
{
"type": "urn:problem-type:cbss:input-validation:example:invalidPassword",
"title": "Invalid password",
"in": "query",
"name": "password",
"value": "oops"
}
]
}
Extending the RequestValidator
You may have a need to extend the RequestValidator with custom organization- or domain-specific validations.
This is possible by extending the AbstractRequestValidator base class.
If in turn, you want your own validator implementation to be extensible as well, you can propagate the extensible fluent builder pattern as follows:
public abstract class AbstractMyRequestValidator<SELF extends AbstractMyRequestValidator<SELF>>
extends AbstractRequestValidator<SELF> {
public SELF something(Input<String> input) {
addValidator(new SomethingValidator(input));
return getThis();
}
}
public final class MyRequestValidator extends AbstractMyRequestValidator<MyRequestValidator> {
}
6. Implementation details
This section describes the internal implementation details of this library.
6.1. @ProblemType and ProblemTypeRegistry
This library makes use of Jackson’s polymorphic deserialization feature, so the Jackson ObjectMapper knows which problem type to instantiate when deserializing a problem.
The problem subclasses have their problem type URI configured in the
@ProblemType
annotation.
These are discovered automagically by a ProblemTypeRegistry
implementation.
-
in a Jakarta EE container, this is done by the
CdiProblemTypeRegistry
CDI extension, integrated with the Jackson ObjectMapper by the CdiProblemModule. -
in a Spring Boot container, this is done by the
SpringProblemTypeRegistry
, integrated with the Jackson ObjectMapper by the SpringProblemModule.
When deserializing a problem for which no corresponding @ProblemType
was found on the classpath, a DefaultProblem is used as fallback.
6.2. belgif-rest-problem-java-ee
This module provides components that handle Java EE / Jakarta EE integration with the belgif-rest-problem library:
-
CdiProblemTypeRegistry: ProblemTypeRegistry implementation that uses CDI component scanning to detect @ProblemType annotations.
-
CdiProblemModule: a Jackson Module that registers the CdiProblemTypeRegistry.
-
ProblemObjectMapperContextResolver: a JAX-RS ContextResolver for a Jackson ObjectMapper with the CdiProblemModule registered. Its priority of "Priorities.USER + 200" allows it to be overridden if needed by the client application.
-
ConstraintViolationExceptionMapper: a JAX-RS ExceptionMapper that converts ConstraintViolationException to an HTTP 400 BadRequestProblem.
-
ProblemExceptionMapper: a JAX-RS ExceptionMapper that converts Problem exceptions to a proper application/problem+json response.
-
WebApplicationExceptionMapper: a JAX-RS ExceptionMapper that handles WebApplicationExceptions thrown by the JAX-RS runtime itself, to prevent them from being handled by the DefaultExceptionMapper.
-
DefaultExceptionMapper: a JAX-RS ExceptionMapper that converts any other uncaught exception to an HTTP 500 InternalServerErrorProblem.
-
EJBExceptionMapper: a JAX-RS ExceptionMapper that handles EJBExceptions. Routes to ProblemExceptionMapper when the EJBException is caused by a Problem exception. Otherwise, routes to DefaultExceptionMapper.
-
JaxRsParameterNameProvider: a bean validation ParameterNameProvider that retrieves parameter names from JAX-RS annotations
-
JAX-RS Client integration:
-
ProblemClientResponseFilter: JAX-RS ClientResponseFilter that converts problem response to a ProblemWrapper exception.
In accordance with the spec, any exception thrown by a JAX-RS ClientResponseFilter gets wrapped in a ResponseProcessingException, unless the exception itself is a ResponseProcessingException. For that reason, the ProblemClientResponseFilter wraps the Problem exception in a "ProblemWrapper" class that extends ResponseProcessingException. -
ProblemSupport: for programmatically enabling unwrapping of ProblemWrapper to Problem exceptions on a JAX-RS Client (also works for RESTEasy Proxy Framework clients).
private Client client = ProblemSupport.enable(ClientBuilder.newClient());
-
ProblemClientBuilderProducer: makes ProblemSupport-enabled ClientBuilder and Client beans available for CDI @Inject
@Inject public void setClient(Client client) { this.client = client; }
-
-
MicroProfile REST Client integration:
-
ProblemResponseExceptionMapper: a ResponseExceptionMapper that converts problem responses to Problem exceptions.
-
ProblemRestClientListener: a RestClientListener that registers the ProblemObjectMapperContextResolver and ProblemResponseExceptionMapper.
-
-
Jakarta EE 9+: the main belgif-rest-problem-java-ee artifact targets Java EE (javax package namespace). A secondary artifact that targets Jakarta EE 9+ (jakarta package namespace) is available with
<classifier>jakarta</classifier>
.
6.3. belgif-rest-problem-spring
This module provides auto-configuration for components that handle Spring Boot integration with the belgif-rest-problem library:
-
SpringProblemTypeRegistry: ProblemTypeRegistry implementation that uses classpath scanning to detect @ProblemType annotations. By default, only package
io.github.belgif.rest.problem
is scanned for @ProblemType annotations. Additional packages to scan can be configured in your application.properties:
io.github.belgif.rest.problem.scan-additional-problem-packages=com.acme.custom
-
SpringProblemModule: a Jackson Module that registers the SpringProblemTypeRegistry.
-
ProblemExceptionHandler: an exception handler for RestControllers that handles the response serialization for Problem exceptions, and converts all other uncaught exceptions to an InternalServerErrorProblem.
-
BeanValidationExceptionsHandler: an exception handler for RestControllers that converts bean validation related exceptions to HTTP 400 BadRequestProblem.
-
InvalidRequestExceptionHandler: an exception handler for the Atlassian swagger-request-validator that converts InvalidRequestException to the correct Problem type.
-
RoutingExceptionsHandler: an exception handler for RestControllers that converts routing related validation exceptions to HTTP 400 BadRequestProblem.
-
ProblemResponseErrorHandler: a RestTemplate error handler that converts problem responses to Problem exceptions.
-
ProblemRestTemplateCustomizer: a RestTemplateCustomizer that registers the ProblemResponseErrorHandler.
In general, these components make it possible to use standard java exception handling (throw and try-catch) for dealing with problems in Spring Boot REST APIs.
6.3.1. belgif-rest-problem-spring-boot-2
Rather than depending on belgif-rest-problem-spring directly, Spring Boot 2.x users are recommended to depend on belgif-rest-problem-spring-boot-2
, which adds some Spring Boot 2.x specific integrations:
-
ProblemWebClientCustomizer: a WebClientCustomizer that registers a filter that converts problem responses to Problem exceptions. This handles integration with the Reactive WebClient.
-
AnnotationParameterNameDiscoverer: a bean validation ParameterNameDiscoverer that retrieves parameter names from Spring MVC annotations
-
ProblemValidatorConfiguration: registers a LocalValidatorFactoryBean with the AnnotationParameterNameDiscoverer
6.3.2. belgif-rest-problem-spring-boot-3
Rather than depending on belgif-rest-problem-spring directly, Spring Boot 3.x users are recommended to depend on belgif-rest-problem-spring-boot-3
, which adds some Spring Boot 3.x specific integrations:
-
ProblemWebClientCustomizer: a WebClientCustomizer that registers a filter that converts problem responses to Problem exceptions. This handles integration with the Reactive WebClient.
-
NoResourceFoundExceptionHandler: an exception handler for RestControllers that converts NoResourceFoundException to HTTP 404 ResourceNotFoundProblem.
-
ProblemRestClientCustomizer: a RestClientCustomizer that registers the ProblemResponseErrorHandler.
-
AnnotationParameterNameProvider: a bean validation ParameterNameProvider that retrieves parameter names from Spring MVC annotations
-
ProblemValidationConfigurationCustomizer: a ValidationConfigurationCustomizer that registers the AnnotationParameterNameProvider