Companion code for "Global Exception Handling with ProblemDetail (RFC 9457) in Spring Boot 4". Thirteen failures under five handling setups (Boot defaults, the Boot flag, a ResponseEntityExceptionHandler advice, advice plus an ErrorController, a catch-all ordered first), validation errors, i18n, content negotiation, Security's 401/403, silent 500s and decoding on the client. 16 tests pin the behaviour. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
63 lines
2.8 KiB
Markdown
63 lines
2.8 KiB
Markdown
# 3. Validation errors that say something
|
|
|
|
[← 2. Choosing a mechanism](02-choosing-a-mechanism.md) · [Index](../README.md) · Next: [4. Types and message codes →](04-i18n-and-types.md)
|
|
|
|
Two exceptions, depending on where the constraint is:
|
|
|
|
| Constraint on | Exception | Stock `detail` |
|
|
|---|---|---|
|
|
| a `@Valid @RequestBody` object | `MethodArgumentNotValidException` | `Invalid request content.` |
|
|
| a parameter directly (`@RequestParam @Max(100) int limit`) | `HandlerMethodValidationException` | `Validation failure` |
|
|
|
|
The second is Spring Framework 6.1's built-in method validation: a constraint annotation on a
|
|
controller parameter is enough, no `@Validated` on the class.
|
|
|
|
## The stock body tells the client nothing
|
|
|
|
From [`matrix-boot-flag.txt`](output/matrix-boot-flag.txt) - three violations in the request, none in
|
|
the response:
|
|
|
|
```
|
|
HTTP 400 Content-Type: application/problem+json
|
|
{"detail":"Invalid request content.","instance":"/orders","status":400,"title":"Bad Request"}
|
|
```
|
|
|
|
## Adding the violations
|
|
|
|
Override the protected hook, keep the body the base class built, add an extension member, and hand
|
|
it back to `handleExceptionInternal` so headers and status stay right
|
|
([`GlobalExceptionHandler`](../src/main/java/com/ankurm/problems/advice/GlobalExceptionHandler.java)):
|
|
|
|
```java
|
|
@Override
|
|
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
|
|
HttpHeaders headers, HttpStatusCode status, WebRequest request) {
|
|
ProblemDetail pd = ex.getBody();
|
|
pd.setDetail("The request body has " + ex.getErrorCount() + " invalid field(s)");
|
|
pd.setProperty("errors", ex.getFieldErrors().stream()
|
|
.map(e -> Map.of("pointer", "#/" + e.getField().replace('.', '/'),
|
|
"detail", String.valueOf(e.getDefaultMessage())))
|
|
.toList());
|
|
return handleExceptionInternal(ex, pd, headers, status, request);
|
|
}
|
|
```
|
|
|
|
The `{"pointer","detail"}` shape is not invented: it is the example RFC 9457 itself uses in
|
|
section 3. From [`matrix-advice.txt`](output/matrix-advice.txt):
|
|
|
|
```
|
|
{"detail":"The request body has 3 invalid field(s)","instance":"/orders","status":400,"title":"Bad Request","errors":[{"pointer":"#/quantity","detail":"must be greater than or equal to 1"},{"pointer":"#/customerEmail","detail":"must be a well-formed email address"},{"pointer":"#/sku","detail":"must not be blank"}]}
|
|
```
|
|
|
|
## Do not assert on the order
|
|
|
|
The order in that line is not declaration order, and it differed between runs of the same
|
|
request. The contract
|
|
test asserts membership, not position.
|
|
|
|
## Nested fields
|
|
|
|
`FieldError.getField()` uses dots for nesting (`address.city`) and brackets for indexes
|
|
(`lines[0].sku`). The `replace('.', '/')` here handles the first; if your API has lists of objects,
|
|
convert `[n]` to `/n` as well so the pointer is a valid JSON Pointer.
|