spring-boot: Consider using RFC 7807 problem details for error responses
Right now Spring Boot is using an ad hoc format for error responses. Often, applications are configured to support JSON/XML formats and the error map is serialized with such message converters for machine clients, alternatively errors are rendered as HTML pages for browsers.
We could consider using a better defined format for Spring Boot errors, here the RFC 7807 “problem details” specification. This specification can carry error responses like the following:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
This could improve error handling in several ways.
First, we could add more contextual information to error maps, like "type"
, "title"
and "detail"
and provide application hook points to translate application exceptions to problem details (see the Zalando library for this).
Also, in cases like #19522 and spring-projects/spring-framework#23421, it could allow HTTP clients to add the specific media type “application/problem+json” to their “Accept” header - and would disambiguate about the format to use when rendering errors. Right now, “application/json” and “application/xml” are the common ones but it’s hard to differentiate between application payload and error payloads.
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 68
- Comments: 25 (13 by maintainers)
At bol.com, the biggest online retailer in the Netherlands and Belgium, RFC 7807 is our bread and butter. Judging from the amount of work Zalando has also put into this (problem and problem-spring-web), I think we are not alone in this effort. This said, there is a significant amount of work that we needed to implement to make
Problem
s work with Spring. Here I want to share some highlights from our iceberg.Model
In our custom
Problem
model, we enhance the one in RFC 7807 with the following fields:String host
– The host on which this problem has occurred.List<Violation> violations
– The list of constraint violations.Problem cause
– The underlying cause, if any.Map<String, Object> metadata
– Collection of fields that are unknown during deserialization.Problem
is an interface. Its default implementation isDefaultProblem
accessible viaProblem.builder()
.DefaultProblem
extends fromAbstractThrowableProblem
, which extends fromRuntimeException
and implementsProblem
.There are, IIRC, at least 3 Jackson bugs one need to work around to make serialization work using annotations. For instance, if
cause
extends fromThrowable
, givenThrowable
isfinal
, quite some mixin magic is needed to avoid serializing stack traces and such. Hence, the necessary (de)serialization plumbing is far from trivial.Models are published in a standalone artifact with only Jackson dependency.
Integration
We encourage our programmers to always throw standard exceptions (e.g.,
RuntimeException
) in their business logic. If need arises, they are only advised to catch exceptions at the controller level and map them toProblem
s manually. Put another way, we only advise explicit use ofProblem
s to communicate a failure if current mappers fall short of addressing the need; else, standard exceptions are the way to go.Mapping exceptions to
Problem
sOur plethora of Spring sauce does its best to catch thrown exceptions and create a meaningful
Problem
out of that. We have custom mappers for a variety of Spring exceptions:BindException
ConstraintViolationException
HttpMediaTypeNotAcceptableException
HttpMediaTypeNotSupportedException
HttpMessageNotReadableException
HttpRequestMethodNotSupportedException
MethodArgumentNotValidException
MethodArgumentTypeMismatchException
MethodNotAllowedException
MissingServletRequestParameterException
NotAcceptableStatusException
ResponseStatusException
ServerWebInputException
UnsupportedMediaTypeStatusException
WebExchangeBindException
While this works 99% of the cases, it indeed is a moving target. Yet, it works pretty good so far for us.
Logging exceptions
This is the curse and bliss of our implementation. We support logging of
Problem
s. That is, whenever a REST controller responds with aProblem
, we log it. The logging behaviour can be configured to set the logging level and filter ontype
. For instance, “logProblem
s oftype
/problems/unhandled-exception
atERROR
level”.Our users really much depend on this feature and love it! Yet, it is practically impossible to cover every case. There are a couple of bug reports with really weird combinations (e.g., “exceptions thrown from exception handlers in a Web MVC controller fails to log the response
Problem
”) we still need to figure out how to tackle. This issue is simply solved via introducing a bean extending fromHandlerResultHandler
and access to the response model viaHandlerResult#getReturnValue()
. Though this only works for the WebFlux backend. Web MVC doesn’t have such a global interceptor where one can access to the response model. WebFlux’sHandlerResultHandler
equivalent in Web MVC isHandlerInterceptor
and that only exposes the raw I/O stream. Long story short, interceptingProblem
s via a global handler that works with both WebFlux and Web MVC is not possible at this stage, to the best of our knowledge.Conclusion
We are totally unhappy with the myriad of enhancements we need to add to make
Problem
s work. Worse, occassionally we stumble upon corner cases that doesn’t work. We will be extremely happy to see this feature being shipped by Spring itself. I also need to note that we can afford company time to contribute to this effort./cc @lkleeven @mzeijen @breun @sagacity
My two cents regarding JSON errors in general: in basically every project using Spring Boot for HTTP services, and a JavaScript framework (such as Angular or Vue), I always need to customize the JSON error sent by Spring Boot in order to
{ "errorKey": "error.accountBanalnceTooLow", errorParams: { "currentBalance": 100 } }
)I haven’t read the RFC very carefully yet, but if Spring Boot could provide a standard way to support this common (in my experience) need, it would be great. Or at least not lose the possibility to do it ourselves, and maybe document what the best strategy is to do it by respecting the standard.
I like to map exceptions to rfc-7807 compliant bodies on the server side and/or back to exceptions on the client side (note: they don’t have to be the same exception, they only have to be compatible). I defined an API and wrote an implementation where you can rely on useful defaults for the fields or annotate your exceptions when you have specific requirements: https://github.com/t1/problem-details
I’ve blogged about the topic in more detail: https://blog.codecentric.de/en/2020/01/rfc-7807-problem-details-with-spring-boot-and-jax-rs/
I also created a PR for Microprofile Sandbox, as I think it should be a common standard.
I would be glad to contribute the Spring Boot implementation.
In order to make the client side feel more organic, maybe we would need to scan for all exceptions?
Any feedback (except for silence 😉 is welcome!
I’ve created an issue in the Spring Framework https://github.com/spring-projects/spring-framework/issues/27052 for this.
Spring HATEOAS provides basic support for RFC-7807 since 1.1, see spring-projects/spring-hateoas#1057 and the relevant chapter of reference manual.
Now, this might not mean too much for Spring (Boot) users that don’t use Spring HATEOAS, but since RFC-7807 is IMO a generally very useful spec, I wonder whether this basic support could be moved to Spring Framework proper? Because it is something that has a clear use even without the involvement of Spring HATEOAS and/or Spring Boot. Both could then leverage the Framework support and build their own extended support on top of it. WDYT @bclozel and @gregturn?
Also, seeing this was twice marked as
for: team-attention
, could Spring Boot team share outcome of those discussions?I don’t find the security angle a very compelling argument against supporting RFC 7807 Problems. I’ve been using Problem JSON for a couple of years now in a micro services architecture with many hundreds of REST API applications, and in my experience developers that come across Problem JSON mostly start producing errors that are way clearer and easier to understand for users. Over-disclosure can happen through any format, and a plain text stack trace is generally way worse than a Problem JSON object. I don’t feel that we shouldn’t have nice things just because they could contain information that shouldn’t be exposed. Not having a nice standard for errors isn’t a way to get more secure applications. Things like training, awareness, testing, etc. are what will get you more secure applications.
Just to be clear, i was posting the argument because I thought it was worth analyzing. It deserves a response. But that doesn’t mean I agree with it. I think the benefits of a general mechanism outweigh the drawbacks.
It’s an old dispute going forth and back: should we prevent good things just because they can be misused? Don’t get me wrong: this dispute is difficult and there is no simple answer. But in this case, I stand clearly on one side, because it’s a very good thing and the risk is not seriously big. And best of all, we can even implement it in a way that doesn’t reveal anything by default, so developers have to actively decide what they want to be exposed… and they do that every day with non-exceptional code — I think they can handle it.
using latest SB 3.1 with webflux, I can successfully use problem details when raising
ResponseStatusException
and can successfully handle other exceptions with@ExceptionHandler
. But if I make a rest request that doesn’t match any of my controllers the server responds with the old format and the only way I found to fix this was by addingspring.web.resources.add-mappings=false
in myapplication.properties
file. I tried to understand this but couldn’t find any useful explanations in the docs, internet and even spent several hours digging into spring’s source code but I still cannot figure out why this is happening and how to “change” this outcome without having to disable the resources mappings. any ideas?update: interesting thing… I managed to fix this by adding
spring.webflux.static-path-pattern=/resources/**
to myapplication.properties
- so now if I make a request to a non-existing resource it returns the correct ProblemDetails response… even if I request a non-existing resource to/resources/**
it comes back correctly. So I’m wondering if that’s a bug 🤔I didn’t see much difference either, maybe the references in documentation/code to RFC 7807 should be updated in order to reference the new RFC 9457? To highlight the support of the new non-obsoleted RFC.
As RFC 7807 has been obsoleted by RFC 9457:
@rstoyanchev @bclozel are there any plans to support the format defined in the new RFC 9457?
I couldn’t find any reference to it either in Spring Framework repository nor in this Spring Boot one.
Thanks in advance
Additionally, there is already a precedent in the Spring Boot white label error page which can can be configured to show stack trace information.
@vpavic I don’t think there really was an outcome from the team discussions. We probably flagged it to discuss how easy it would be to get it into a release, but ultimately decided to prioritize other issues.