1. Context
Java library for working with RFC 9457 standardized Belgif problems.
With this library, RFC 9457 Problems can be 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
2. Release notes
2.2. Version 0.5
belgif-rest-problem-spring:
Split into belgif-rest-problem-spring-boot-2 and belgif-rest-problem-spring-boot-3. To benefit from Spring Boot 2.x or 3.x specific features, replace dependencies to belgif-rest-problem-spring by the version-specific variant.
belgif-rest-problem-spring-boot-3:
-
Map NoResourceFoundException to 404
urn:problem-type:belgif:resourceNotFound
-
Added support for RestClient API
2.3. Version 0.4
belgif-rest-problem:
-
Removed deprecated InvalidParamProblem: use InputValidationProblem, which supports both the legacy invalidParams[] and the new issues[] structure
-
Replace specific
urn:problem-type:cbss:input-validation:unknownSsin
issue type by genericurn:problem-type:cbss:input-validation:referencedResourceNotFound
-
Added optional replacedByHref property to replacedSsin issue type
belgif-rest-problem-validator:
-
Added requireIfPresent check for validating input(s) that must be present when a given target input is present
2.4. Version 0.3
belgif-rest-problem:
-
Removed deprecated "status" and "instance" properties from InputValidationIssue
belgif-rest-problem-spring:
-
Added support for bean validation
-
Added Jakarta EE 9/10 support: use
<classifier>jakarta</classifier>
2.5. Version 0.2
belgif-rest-problem:
-
Extracted RequestValidator to separate belgif-rest-problem-validator module.
-
Added equals() and hashCode() to all Problem classes
-
Move additionalProperties from DefaultProblem to Problem
belgif-rest-problem-java-ee:
-
Added Jakarta EE 9/10 support: use
<classifier>jakarta</classifier>
belgif-rest-problem-spring:
-
Remove
be.fgov.kszbcss
from default scanned problem type packages -
Rename
io.github.belgif.rest.problem.spring.scan-additional-problem-packages
configuration property toio.github.belgif.rest.problem.scan-additional-problem-packages
documentation:
-
Add chapter on Code generators
3. belgif-rest-problem
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)
3.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)
|
3.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.
|
3.3. @ProblemType and ProblemTypeRegistry
We make 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 Java 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.
4. 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"
}
]
}
5. 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.
-
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 ProblemClientResponseFilter. -
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>
.
5.1. Using rest-problem library for implementing REST services
When implementing a REST service with JAX-RS, returning a problem is as simple as throwing the problem exception from your code:
throw new BadRequestProblem(
new InputValidationIssue(InEnum.PATH, "sectorCode", sectorCode)
.title("Invalid sector code"));
5.2. Using rest-problem library for consuming REST services
When using the MicroProfile REST Client API, handling a problem is as simple as catching the problem exception in your code:
try {
service.call();
} 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 (WebApplicationException e) {
LOGGER.info(e.getMessage());
// handle all other WebApplicationException
}
If the service returns a problem type for which you do not have a corresponding @ProblemType on your classpath, a DefaultProblem is used instead.
|
6. 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.
-
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.
-
ProblemWebClientCustomizer: a WebClientCustomizer that registers a filter that converts problem responses to Problem exceptions. This handles integration with the Reactive WebClient.
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.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.
6.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:
-
NoResourceFoundExceptionHandler: an exception handler for RestControllers that converts NoResourceFoundException to HTTP 404 ResourceNotFoundProblem.
-
ProblemRestClientCustomizer: a RestClientCustomizer that registers the ProblemResponseErrorHandler.
7. Code generators
Code generators should be configured to use the belgif-rest-problem types for problem-related model classes, instead of generating them.
7.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>
7.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>