-
Notifications
You must be signed in to change notification settings - Fork 61
feat: Support ProblemDetail #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
In the issue discussion, you proposed to map For someone trying to migrate to the For example, if an instance of this exception is thrown public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(UserId userId) {
super("Could not find user with id " + userId);
}
}Previous to v5.1.0, with the default configuration this would return the following JSON with a 500 HTTP status {
"code": "USER_NOT_FOUND",
"message": "Could not find user with id 123"
}In v5.1.0, with {
"type": "user-not-found",
"detail": "Could not find user with id 123",
"title": "???"
}But what would the It would be helpful to see some examples of the
|
| .contentType(MediaType.APPLICATION_JSON) | ||
| .body(BodyInserters.fromValue(errorResponse)); | ||
| if (errorHandlingProperties.isUseProblemDetailFormat()) { | ||
| return ServerResponse.status(Objects.requireNonNull(errorResponse.getHttpStatus())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the httpStatus in ApiErrorResponse be defined as non-null to avoid all the calls to Objects.requireNotNull?
Maybe there are some places (outside this PR), where getHttpStatus() is expected to return null that I've missed. If so, an alternative approach is to change getHttpStatus() to return Optional<HttpStatusCode> which would allow the @Nullable to be removed from the getter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this is not possible.
The status field is not always present in the JSON output (it is mostly not present as this is the default). So when you want to serialize the JSON back to the object (using ApiErrorResponseDeserializer), then it can be null.
Using Optional would interfere with existing code. I am also not sure if it would influence the JSON serialization. This is something that could be considered for a next major version, but I don't want to do breaking changes at this point.
src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/ProblemDetailFactory.java
Outdated
Show resolved
Hide resolved
| if (errorHandlingProperties.isUseProblemDetailFormat()) { | ||
| ProblemDetail problemDetail = problemDetailFactory.build(errorResponse); | ||
| return ResponseEntity.status(Objects.requireNonNull(errorResponse.getHttpStatus())) | ||
| .contentType(MediaType.APPLICATION_PROBLEM_JSON) | ||
| .body(problemDetail); | ||
| } else { | ||
| return ResponseEntity.status(Objects.requireNonNull(errorResponse.getHttpStatus())) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .body(errorResponse); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if-else block is very similar to the if-else in GlobalErrorWebExceptionHandler.handleException.
Perhaps it could be improved (in terms of encapsulation/DRY) by defining a custom type for rendering the response, e.g.
public interface ResponseFactory {
ServerResponse buildReactiveResponse(ApiErrorResponse error);
ResponseEntity<?> buildServletResponse(ApiErrorResponse error);
}and have 2 beans that implement this interface
DefaultResponseFactory- when ProblemDetail is disabledProblemDetailResponseFactory- when ProblemDetail is enabled
Inject the appropriate implementation of the interface into GlobalErrorWebExceptionHandler and ErrorHandlingControllerAdvice and use it instead of ProblemDetailFactory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea, but what I don't like about it is that it mixes reactive and servlet classes in the same interface. i rather keep those parts separate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two sources of variation
- Reactive or servlet
- ProblemDetail enabled or disabled
In the proposal above, (1) is handled via separate interface methods and (2) is handled via separate implementations of the interface
If you would prefer not to mix servlet and reactive code in the same type, perhaps it would be better to swap (1) and (2). In other words, define an interface with one method for generating the ProblemDetail enabled response, a second method for the ProblemDetail disabled response, and separate implementations of the interface for the reactive and servlet cases.
There are many other options for handling the various cases, but I do think that moving the response-generation to a dedicated type would be a cleaner design and eliminate some code duplication.
| private String toKebabCase(String input) { | ||
| if (input.isEmpty()) { | ||
| return input; | ||
| } | ||
|
|
||
| String result = input | ||
| .replaceAll("([a-z])([A-Z])", "$1-$2") // camelCase boundaries | ||
| .replaceAll("([A-Z])([A-Z][a-z])", "$1-$2") // handle acronyms | ||
| .replaceAll("\\s+", "-") // spaces to hyphens | ||
| .replaceAll("_", "-") // underscores to hyphens | ||
| .toLowerCase(); | ||
| LOGGER.info("{} -> {}", input, result); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless the ProblemDetail spec requires the type field to be kebab case, It's not clear why you're changing the code to kebab case when ProblemDetail mode is enabled.
This will make it more difficult to migrate to ProblemDetail mode (e.g. changing frontend code), for no obvious benefit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the examples I have found use kebab case for the type field. I guess because it is supposed to be a URL?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK, there's nothing that prohibits the use of uppercase or underscores in a URL, but obviously it's much more common to use lowercase and dashes. So you could argue that lowercase and dash-separators are a de facto standard.
Presumably a lot of applications using this starter will have switch-statements or if-else statements that check the type of the error, which will need to be migrated if ProblemDetail support is enabled. I can see 3 options
- Stick with kebab case and mention (e.g. in the release notes) that the aforementioned migration will be necessary if ProblemDetail support is enabled
- Use SNAKE_CASE for the error type in all cases
- Use kebab case (1) by default, but provide a flag that allows SNAKE_CASE (2) to be used instead. Obviously, this is the option that provides the most flexibility for the user, but the most amount of effort for you
Personally, I think all 3 options are defensible, and even if someone does have to migrate, it's a pretty easy migration, so probably not worth worrying about too much.
|
As a reply to #128 (comment): I tested with the So title is really coming from the status code. If I annotated with For a validation error, status 400 is used and title is |
It would be helpful to mention this in the docs, and perhaps also include an example |
This PR adds support for ProblemDetail.
It is disabled by default and can be enabled via the
error.handling.use-problem-detail-format=trueproperty.Mapping is done like this:
type: Is thecode, but converted to kebab case (e.g.validation-failed). Optionally, you can give the url a prefix viaerror.handling.problem-detail-type-prefixproperty (e.g.https://example.com/validation-failed).title: Is generated automatically by the SpringProblemDetailclass.detail: Is themessage.Additional properties (added via
@ResponseErrorProperty) are also added to the result. For validation errors, thefieldErrorsandglobalErrorswill also be present.Fixes #59