①: We use the Lombok @RequiredArgsConstructor to create a constructor to be auto-wired.
The OrderService may throw an UserNotEntitledToOrderOnAccountException.
Spring Boot already provides a json error body by default, but it's very technical. It contains these fields:
status+error: e.g.403andForbiddenmessage: e.g.You're not entitled to use this payment methodpath: e.g./orderstimestamp: e.g.2020-01-10T12:00:00.000+0000trace: the stacktrace
We need to specify the http status code and message by annotating the
UserNotEntitledToOrderOnAccountException:
Note that there is no stable field to distinguish different error situations, our main use-case. So we need to take a different route:
Manual Exception Mapping
The most basic approach is to catch and map the exception manually, i.e. in our OrderBoundary we return a ResponseEntity with one of two different body types: either the shipment or the problem detail:
type field, e.g. to a Wiki. ②: I chose to use a random UUID URN for the
instance.③: I log the problem detail and the stack trace, so we can search our logs for the UUID
instance to see all details in the context of the logs that led to the problem.Problem Detail
The ProblemDetail class is trivial (thanks to Lombok):
Exception Handler
This manual mapping code can grow quite a bit if you have many exceptions to convert. By using some conventions, we can replace it with a generic mapping for all our exceptions. We can revert the OrderBoundary to the simple form and use an exception handler controller advice instead:
②: We handle all exceptions and errors.
③: We log the details (including the
instance) and the stack trace.
The interesting part is in the ProblemDetailBuilder.
Problem Detail Builder
The conventions used here are:
type: URL to the javadoc of the exception hosted onhttps://api.myshop.example/apidocs. This may not be the most stable URL, but it's okay for this demo.title: Use the simple class name, converting camel case to spaces.detail: The exception message.instance: Use a random UUID URN.status: If the exception is annotated asStatususe that; otherwise use a500 Internal Server Error.
Note that you should be very careful with conventions: they should never bear any surprises.The ProblemDetailBuilder is a few lines of code, but it should be fun to read:
You can extract this error handling into a separate module, and if you can agree on the same conventions with other teams, you can share it. You may even simply use a problem detail artifact defined by someone else, like mine 😜, which also allows extension fields and other things.
Client
I don't want to spill technical details all over my domain code, so I extract an OrderServiceClient class to do the call and map those problem details back to exceptions. I want the domain code to look something like this:
So the interesting part is in the OrderServiceClient.
Manual Problem Detail Mapping
Leaving the error handling aside, the code doesn't look too bad:
Response Error Handler
There's also a mechanism on the Spring REST client side that allows us to generalize this handling:
try-catch block.
The ProblemDetailErrorHandler hides all the conventions we use; this time including some error handling. In that case, we log a warning and fall back to the Spring default handling:
Recovering the exception type from the URL is not ideal, as it tightly couples the client side to the server side, i.e. it assumes that we use the same classes in the same packages. It's good enough for the demo, but to do it properly you need a way to register exceptions or scan for them, like in my library, which also allows extension fields and other things.
JAX-RS
If you're not into JAX-RS, you may want to skip ahead to the Summary.
Server
Say you have a REST boundary OrderBoundary like this:
The OrderService may throw an UserNotEntitledToOrderOnAccountException and we want to map that to a problem detail.
Manual Exception Mapping
The most basic approach is to map it manually, i.e. we return a Response with one of two different body types: the shipment or the problem detail:
type field, e.g. to a Wiki.②: I chose to use a random UUID URN for the
instance.③: I log the problem detail and the stack trace, so we can search our logs for the
instance UUID to see all details in the context of the logs that led to the problem.
The ProblemDetail class is trivial (shown above).
Exception Mapper
This manual mapping code can grow quite a bit if you have many exceptions to convert. By using some conventions, we can replace it with a generic mapping for all our exceptions:
②: We handle all exceptions and errors.
③: We log the details (including the
instance) and the stack trace.
The interesting part is again in the ProblemDetailBuilder shown above.
Client
I don't want to spill technical details all over my domain code, so I extract an OrderServiceClient class to do the call and map those problem details back to exceptions. I want the domain code to look something like this:
So the interesting part is in the OrderServiceClient.
Manual Problem Detail Mapping
The code is quite straight forward:
Response Error Handler
There's also a mechanism on the JAX-RS client side that allows us to generalize this handling:
We completely removed the problem detail handling and extracted it into an automatically registered ClientResponseFilter instead (see ProblemDetailClientResponseFilter further down). The downside of using the JAX-RS client directly is that exceptions thrown by a ClientResponseFilter are wrapped into a ResponseProcessingException, so we need to unpack it. We don't have to do that when we use a MicroProfile Rest Client instead:
The ProblemDetailClientResponseFilter hides all the conventions we use:
ClientResponseFilter with JAX-RS.
②: Recovering the exception type from the javadoc URL is not ideal, as it tightly couples the client side to the server side, i.e. it assumes that we use the same classes in the same packages. It's good enough for the demo, but to do it properly you need a way to register exceptions or scan for them, like in my library, which also allows extension fields and other things.
Summary
Avoid misusing http status codes; that's a snake pit. Produce standardized and thereby interoperable problem details instead, it's easier than you may think. To not litter your business logic code, you can use exceptions, on the server side as well as on the client side. Most of the code can even be made generic and reused in several applications, by introducing some conventions.
This implementation provides annotations for @Type, @Title, @Status, @Instance, @Detail, and @Extension for the your custom exceptions. It works with Spring Boot as well as JAX-RS and MicroProfile Rest Client. Zalando took a different approach with their Problem library and the Spring integration. problem4j looks usable, too. There are solutions for a few other languages, e.g. on GitHub rfc7807 and rfc-7807.
More on this topic by my colleague Christian in his blog post (in German).
What do you think? Do you know about other good libraries? Shouldn't this become a standard tool in your belt?
Blog author
Rüdiger zu Dohna
IT Consulting Expert
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.