Most APIs have a way to report problems and errors, helping the user understand when something went wrong and what the issue is. The method used depends on the API’s style, technology, and design. Handling error reporting is an important part of the overall API design process.
You could create your own error-reporting system, but that takes time and effort, both for the designer and for users who need to learn the custom approach. Thankfully, there’s a standard called IETF RFC 7807 (later refined in RFC 9457) that can help.
By adopting RFC 7807, API designers don’t have to spend time creating a custom solution, and users benefit by recognizing a familiar format across different APIs.
If it suits the API’s needs, using this standard benefits both designers and users alike.
Jooby provides built-in support for Problem Details.
To enable the ProblemDetails, simply add the following line to your configuration:
problem.details.enabled = trueThis is the bare minimal configuration you need. It enables a global error handler that catches all exceptions, transforms them into Problem Details compliant format and renders the response based on the Accept header value. It also sets the appropriate content-type in response (e.g. application/problem+json, application/problem+xml)
All supported settings include:
problem.details {
enabled = true
log4xxErrors = true // (1)
muteCodes = [401, 403] // (2)
muteTypes = ["com.example.MyMutedException"] // (3)
}-
By default, only server errors (5xx) will be logged. You can optionally enable the logging of client errors (4xx). If
DEBUGlogging level is enabled, the log will contain a stacktrace as well. -
You can optionally mute some status codes completely.
-
You can optionally mute some exceptions logging completely.
HttpProblem class represents the RFC 7807 model. It is the main entity you need to work with to produce the problem.
There are several handy static methods to produce a simple HttpProblem:
-
HttpProblem.valueOf(StatusCode status)- will pick the title by status code. Don’t overuse it, the problem should have meaningfultitleanddetailwhen possible. -
HttpProblem.valueOf(StatusCode status, String title)- with customtitle -
HttpProblem.valueOf(StatusCode status, String title, String detail)- withtitleanddetail
HttpProblem extends RuntimeException so you can naturally throw it (as you do with exceptions):
import io.jooby.problem.HttpProblem;
get("/users/{userId}", ctx -> {
var userId = ctx.path("userId").value();
User user = userRepository.findUser(userId);
if (user == null) {
throw HttpProblem.valueOf(StatusCode.NOT_FOUND,
"User Not Found",
"User with ID %s was not found in the system.".formatted(userId)
);
}
...
});import io.jooby.problem.HttpProblem
get("/users/{userId}") { ctx ->
val userId = ctx.path("userId").value()
val user = userRepository.findUser(userId)
if (user == null) {
throw HttpProblem.valueOf(StatusCode.NOT_FOUND,
"User Not Found",
"User with ID $userId was not found in the system."
)
}
...
})Resulting response:
{
"timestamp": "2024-10-05T14:10:41.648933100Z",
"type": "about:blank",
"title": "User Not Found",
"status": 404,
"detail": "User with ID 123 was not found in the system.",
"instance": null
}Use builder to create a rich problem instance with all properties:
throw HttpProblem.builder()
.type(URI.create("http://example.com/invalid-params"))
.title("Invalid input parameters")
.status(StatusCode.UNPROCESSABLE_ENTITY)
.detail("'Name' may not be empty")
.instance(URI.create("http://example.com/invalid-params/3325"))
.build();RFC 7807 has a simple extension model: APIs are free to add any other properties to the problem details object, so all properties other than the five ones listed above are extensions.
However, variadic root level fields are usually not very convenient for (de)serialization (especially in statically typed languages). That’s why HttpProblem implementation grabs all extensions under a single root field parameters. You can add parameters using builder like this:
throw HttpProblem.builder()
.title("Order not found")
.status(StatusCode.NOT_FOUND)
.detail("Order with ID $orderId could not be processed because it is missing or invalid.")
.param("reason", "Order ID format incorrect or order does not exist.")
.param("suggestion", "Please check the order ID and try again")
.param("supportReference", "/support")
.build();Resulting response:
{
"timestamp": "2024-10-06T07:34:06.643235500Z",
"type": "about:blank",
"title": "Order not found",
"status": 404,
"detail": "Order with ID $orderId could not be processed because it is missing or invalid.",
"instance": null,
"parameters": {
"reason": "Order ID format incorrect or order does not exist.",
"suggestion": "Please check the order ID and try again",
"supportReference": "/support"
}
}Some HTTP codes (like 413 or 426) require additional response headers, or it may be required by third-party system/integration. HttpProblem support additional headers in response:
throw HttpProblem.builder()
.title("Invalid input parameters")
.status(StatusCode.UNPROCESSABLE_ENTITY)
.header("my-string-header", "string")
.header("my-int-header", 100)
.build();RFC 9457 finally described how errors should be delivered in HTTP APIs.
It is basically another extension errors on a root level. Adding errors is straight-forward using error() or errors() for bulk addition in builder:
throw HttpProblem.builder()
...
.error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
.error(new HttpProblem.Error("Last name is required", "/lastName"))
.build();In response:
{
...
"errors": [
{
"detail": "First name cannot be blank",
"pointer": "/firstName"
},
{
"detail": "Last name is required",
"pointer": "/lastName"
}
]
}|
Tip
|
If you need to enrich errors with more information feel free to extend |
Apparently, you may already have many custom Exception classes in the codebase, and you want to make them Problem Details compliant without complete re-write. You can achieve this by implementing HttpProblemMappable interface. It allows you to control how exceptions should be transformed into HttpProblem if default behaviour doesn’t suite your needs:
import io.jooby.problem.HttpProblemMappable;
public class MyException implements HttpProblemMappable {
public HttpProblem toHttpProblem() {
return HttpProblem.builder()
...
build();
}
}Extending HttpProblem and utilizing builder functionality makes it really easy:
public class OutOfStockProblem extends HttpProblem {
private static final URI TYPE = URI.create("https://example.org/out-of-stock");
public OutOfStockProblem(final String product) {
super(builder()
.type(TYPE)
.title("Out of Stock")
.status(StatusCode.BAD_REQUEST)
.detail(String.format("'%s' is no longer available", product))
.param("suggestions", List.of("Coffee Grinder MX-17", "Coffee Grinder MX-25"))
);
}
}All the features described above should give you ability to rely solely on built-in global error handler. But, in case you still need custom exception handler for some reason, you still can do it:
{
...
error(MyCustomException.class, (ctx, cause, code) -> {
MyCustomException ex = (MyCustomException) cause;
HttpProblem problem = ... ; // (1)
ctx.getRouter().getErrorHandler().apply(ctx, problem, code); // (2)
});
}-
Transform exception to
HttpProblem -
Propagate the problem to
ProblemDetailsHandler. It will handle the rest.
|
Important
|
Do not attempt to render |