1@RestController
2@RequestMapping(path = "/orders")
3@RequiredArgsConstructor ①
4public class OrderBoundary {
5 private final OrderService service;
6 @PostMapping
7 public Shipment order(@RequestParam("article") String article) {
8 return service.order(article);
9 }
10}
①: 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.403
andForbidden
message
: e.g.You're not entitled to use this payment method
path
: e.g./orders
timestamp
: e.g.2020-01-10T12:00:00.000+0000
trace
: the stacktrace
We need to specify the http status code and message by annotating the
UserNotEntitledToOrderOnAccountException
:
1@ResponseStatus(code = FORBIDDEN,
2 reason = "You're not entitled to use this payment method")
3public class UserNotEntitledToOrderOnAccountException
4 extends RuntimeException {
5 ...
6}
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:
1public class OrderBoundary {
2 @PostMapping
3 public ResponseEntity<?> order(@RequestParam("article") String article) {
4 try {
5 Shipment shipment = service.order(article);
6 return ResponseEntity.ok(shipment);
7 } catch (UserNotEntitledToOrderOnAccountException e) {
8 ProblemDetail detail = new ProblemDetail();
9 detail.setType(URI.create("https://api.myshop.example/problems/" +
10 "not-entitled-for-payment-method")); ①
11 detail.setTitle("You're not entitled to use this payment method");
12 detail.setInstance(URI.create(
13 "urn:uuid:" + UUID.randomUUID())); ②
14
15
1}
①: I chose to use a fixed URL for the 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):
1@Data
2public class ProblemDetail {
3 public static final MediaType JSON_MEDIA_TYPE =
4 MediaType.valueOf("application/problem+json");
5 private URI type;
6 private String title;
7 private String detail;
8 private Integer status;
9 private URI instance;
10}
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:
1@Slf4j
2@ControllerAdvice ①
3public class ProblemDetailControllerAdvice {
4 @ExceptionHandler(Throwable.class) ②
5 public ResponseEntity<?> toProblemDetail(Throwable throwable) {
6 ProblemDetail detail = new ProblemDetailBuilder(throwable).build();
7
8
1}
①: Make the actual exception handler method discoverable by Spring.②: 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 asStatus
use that; otherwise use a500 Internal Server Error
.
1@Retention(RUNTIME)
2@Target(TYPE)
3public @interface Status {
4 int value();
5}
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:
1@RequiredArgsConstructor
2class ProblemDetailBuilder {
3 private final Throwable throwable;
4
5
1}
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:
1@RequiredArgsConstructor
2public class MyApplication {
3 private final OrderServiceClient client;
4 public OrderStatus handleOrder(String articleId) {
5 try {
6 Shipment shipment = client.postOrder(articleId);
7 // store shipment
8 return SHIPPED;
9 } catch (UserNotEntitledToOrderOnAccount e) {
10 return NOT_ENTITLED_TO_ORDER_ON_ACCOUNT;
11 }
12 }
13}
So the interesting part is in the OrderServiceClient
.
Manual Problem Detail Mapping
Leaving the error handling aside, the code doesn't look too bad:
1public class OrderServiceClient {
2 public Shipment postOrder(String article) {
3 MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
4 form.add("article", article);
5 RestTemplate template = new RestTemplate();
6 try {
7 return template.postForObject(BASE_URI + "/orders", form, Shipment.class);
8 } catch (HttpStatusCodeException e) {
9 String json = e.getResponseBodyAsString();
10 ProblemDetail problemDetail = MAPPER.readValue(json, ProblemDetail.class);
11 log.info("got {}", problemDetail);
12 switch (problemDetail.getType().toString()) {
13 case "https://api.myshop.example/apidocs/com/github/t1/problemdetaildemoapp/" +
14 "OrderService.UserNotEntitledToOrderOnAccount.html":
15 throw new UserNotEntitledToOrderOnAccount();
16 default:
17 log.warn("unknown problem detail type [" +
18 ProblemDetail.class + "]:\n" + json);
19 throw e;
20 }
21 }
22 }
23
24
1}
Response Error Handler
There's also a mechanism on the Spring REST client side that allows us to generalize this handling:
1public class OrderServiceClient {
2 public Shipment postOrder(String article) {
3 MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
4 form.add("article", article);
5 RestTemplate template = new RestTemplate();
6 template.setErrorHandler(new ProblemDetailErrorHandler()); ①
7 return template.postForObject(BASE_URI + "/orders", form,
8 Shipment.class);
9 }
10}
①: This line replaces the 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:
1@Slf4j
2public class ProblemDetailErrorHandler extends DefaultResponseErrorHandler {
3 @Override public void handleError(ClientHttpResponse response) throws IOException {
4 if (ProblemDetail.JSON_MEDIA_TYPE.isCompatibleWith(
5 response.getHeaders().getContentType())) {
6 triggerException(response);
7 }
8 super.handleError(response);
9 }
10
11
1}
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:
1@Path("/orders")
2public class OrderBoundary {
3 @Inject OrderService service;
4 @POST public Shipment order(@FormParam("article") String article) {
5 return service.order(article);
6 }
7}
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:
1@Path("/orders")
2public class OrderBoundary {
3 @Inject OrderService service;
4 @POST public Response order(@FormParam("article") String article) {
5 try {
6 Shipment shipment = service.order(article);
7 return Response.ok(shipment).build();
8 } catch (UserNotEntitledToOrderOnAccount e) {
9 ProblemDetail detail = new ProblemDetail();
10 detail.setType(URI.create("https://api.myshop.example/problems" +
11 "/not-entitled-for-payment-method")); ①
12 detail.setTitle("You're not entitled to use this payment method");
13 detail.setInstance(URI.create("urn:uuid:" + UUID.randomUUID())); ②
14
15
1}
①: I chose to use a fixed URL for the 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:
1@Slf4j
2@Provider ①
3public class ProblemDetailExceptionMapper
4 implements ExceptionMapper { ②
5 @Override public Response toResponse(Throwable throwable) {
6 ProblemDetail detail = new ProblemDetailBuilder(throwable).build();
7
8
1}
①: Automatically register the exception handler method with JAX-RS.②: 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:
1public class MyApplication {
2 @Inject OrderServiceClient client;
3 public ResultEnum handleOrder(String articleId) {
4 try {
5 Shipment shipment = client.postOrder(articleId);
6 // store shipment
7 return SHIPPED;
8 } catch (UserNotEntitledToOrderOnAccount e) {
9 return NOT_ENTITLED_TO_ORDER_ON_ACCOUNT;
10 }
11 }
12}
So the interesting part is in the OrderServiceClient
.
Manual Problem Detail Mapping
The code is quite straight forward:
1@Slf4j
2public class OrderServiceClient {
3 public Shipment postOrder(String article) {
4 Response response = target()
5 .path("/orders").request(APPLICATION_JSON_TYPE)
6 .post(Entity.form(new Form().param("article", article)));
7 if (ProblemDetail.JSON_MEDIA_TYPE.isCompatible(response.getMediaType())) {
8 throw buildProblemDetailException(response);
9 }
10 return response.readEntity(Shipment.class);
11 }
12
13
1}
Response Error Handler
There's also a mechanism on the JAX-RS client side that allows us to generalize this handling:
1public class OrderServiceClient {
2 public Shipment order(String article) {
3 try {
4 Response response = target()
5 .request(APPLICATION_JSON_TYPE)
6 .post(Entity.form(new Form().param("article", article)));
7 return response.readEntity(Shipment.class);
8 } catch (ResponseProcessingException e) {
9 throw (RuntimeException) e.getCause();
10 }
11 }
12}
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:
1public class OrderServiceClient {
2 @Path("/orders")
3 public interface OrderApi {
4 @POST Shipment order(@FormParam("article") String article);
5 }
6
7
1}
The ProblemDetailClientResponseFilter
hides all the conventions we use:
1@Slf4j
2@Provider ①
3public class ProblemDetailClientResponseFilter implements ClientResponseFilter {
4 private static final Jsonb JSONB = JsonbBuilder.create();
5
6
1}
①: Automatically register the 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.