I'm implementing a REST API with a ControllerAdvice
and planning to use RFC 9457-compliant error responses.
I see what Spring Framework 6+ provides.
In Spring 6 documentation I read this: https://docs.spring.io/spring-framework/docs/6.0.0/reference/html/web.html#mvc-ann-rest-exceptions-non-standard
You can also extend ProblemDetail to add dedicated non-standard properties. The copy constructor in ProblemDetail allows a subclass to make it easy to be created from an existing ProblemDetail. This could be done centrally, e.g. from an @ControllerAdvice such as ResponseEntityExceptionHandler that re-creates the ProblemDetail of an exception into a subclass with the additional non-standard fields.
Sounds good: the ResponseEntityExceptionHandler
provides this method which might be overridden to create my own extension instances of ProblemDetail
from the framework-provided ones:
protected ProblemDetail createProblemDetail(
Exception ex, HttpStatusCode status, String defaultDetail, @Nullable String detailMessageCode,
@Nullable Object[] detailMessageArguments, WebRequest request) {
// ...
}
However this method is invoked "for any exception that doesn't implement ErrorResponse
", as confirmed by its Javadoc. When, however, the intercepted exception implements ErrorResponse
, the ProblemDetail
is built inside the handleExceptionInternal
method directly, by using ErrorResponse.updateAndGetBody
, with no call to the above method and so no hook available to transform it into my own problem detail extension.
Perhaps some protected method that gets the ProblemDetail
, the originating exception, the web request, and so on, and returns another ProblemDetail
, invoked in both the above places, could make the job. Default implementation could just return the ProblemDetail
instance as-is, while an extension could decorate it and return a suitable ProblemDetail
extension.
The second doubt I have is this: why does createProblemDetail
receive an Exception
and not a Throwable
? If my ControllerAdvice
extending ResponseEntityExceptionHandler
wants to also handle Error
s (or a generic Throwable
), it can't use it to create a ProblemDetail
. This is in contrast with ErrorResponse.builder()
method, which does accept a Throwable
.
Comment From: rstoyanchev
ResponseEntityExceptionHandler
has one @ExceptionHandler
that handles built-in Exceptions that may be thrown, and for each exception there is a dedicated protected method. That's one opportunity to enrich the body (ProblemDetail
) per exception. For a single place, you can also override handleExceptionInternal
, check if the exception is ErrorResponseException
and re-create its ProblemDetail
with additional properties.
As for createProblemDetail
taking Exception
, the method helps with built-in, non-web exceptions of our own, and it's sufficient for that purpose. For any other (application) exceptions, using ErrorResponse.Builder
directly might be more straight forward, especially if you don't need to support customization via MessageSource
.
What do you actually want to add to responses for built-in exceptions?
Comment From: mauromol
What I mean is: if handleExceptionInternal
were somehow delegating to createProblemDetail
(*), and if the latter were accepting Throwable
, we would have a single point of extension were we could inject our logic to further decorate the standard ProblemDetail
.
Right now, to have ALL error responses be an extension of ProblemDetail
(suppose I want to add a timestamp to it, or any other kind of additional information), I have to add the code to extend the ProblemDetail
creation in three different points:
- in handleExceptionInternal
, because otherwise for Spring exceptions implementing ErrorResponse
I would get plain ProblemDetail
s
- in createProblemDetail
, so that I can intercept any ProblemDetail
creation for built-in exceptions not implementing ErrorResponse
, and/or I can then delegate to this method the creation of problem details while implementing my own custom exception handler methods
- in a newly added createProblemDetail
copy, which supports Throwable
instead of Exception
, if I want to write a "catch all" handler for any other throwable or if I want to write a handler for some Error
, for example
We may discuss whether handling Throwable
s that are not Exception
s is really meaningful (but since ErrorResponse.builder()
does support Throwable
, I would say: why not?), but anyway at least the first two points remain valid for sure.
Not a huge problem, but given the elegance of the Spring Framework in many other places where similar problems were brilliantly solved, it surprised me to see such an inconsistency here, especially because the documentation seems to suggest that this is indeed one possible use case that was expected from the beginning.
(*) when I talk about delegation from handleExceptionInternal
to createProblemDetail
for ErrorResponse
s implementation, I do not mean that handleExceptionInternal
should refrain from using the built-in support given by ErrorResponse
interface to create the ProblemDetail
, I mean that we could have a single extension point/overridable method were a ProblemDetail
(either created by the ErrorResponse
or by the createProblemDetail
method) could be decorated and possibly extended, so that you just need to override that point and nothing else. So one single extension point vs 2 or 3.
Hope I've explained it better, sorry if I've been confusing.
Comment From: rstoyanchev
I get what you're after, but you're missing some parts.
createProblemDetail
is to create a ProblemDetail
from scratch for exceptions that don't expose this, so you need to pass detail
, detailMessageCode
, and detailMessageArguments
into it. An ErrorResponse
already encapsulates the ProblemDetail and additional details, and so it doesn't make sense to re-create it from scratch.
Let's leave that aside, and focus on what you're after. If you want to add a property from a single place, the easiest thing you can do is:
@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode,
WebRequest request) {
if (body instanceof ProblemDetail problemDetail) {
problemDetail.setProperty("timestamp", System.currentTimeMillis());
}
return super.createResponseEntity(body, headers, statusCode, request);
}
}
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: mauromol
Ok, thanks Rossen, I see it now. I find it a bit counter-intuitive that I have to customize the body creation to customize the ProblemDetail
creation, instead of just having a hook where I can enrich or post-process a previously built ProblemDetail
, but the createResponseEntity
can serve the purpose.
Comment From: rstoyanchev
Indeed this has evolved a bit. Originally, there was no ProblemDetail
and no specific format to use, and the class always aimed to be as open for extension as possible. The RFC support made it a little more involved, but it's still quite open with multiple possibilities, which is also part of the issue. Hence my hesitation to add yet another option.
Comment From: rstoyanchev
Closing for now, but thanks for the feedback.