Three companion modules verifying and rewriting the Boot 4.1.1 / Framework 7.0.9 story for three older articles: the javax->jakarta.validation namespace fix plus Jakarta Validation 3.1 record-validation clarification, ETag/ conditional-request APIs re-verified unchanged plus the starter rename, and RestTemplate Basic Auth rebuilt on RestClient with the exchange() trap called out. 19 real passing tests generate every transcript quoted from the three companion articles. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EQNA6DJ9VgCtW6zhCE8Xud
78 lines
4.5 KiB
Markdown
78 lines
4.5 KiB
Markdown
# 3. What the defaults do not do: record validation failures with no error body
|
|
|
|
[Prev: Record validation](02-record-validation.md) | [README](../README.md)
|
|
|
|
Source: [`ContactController.java`](../src/main/java/com/ankurm/customvalidation/web/ContactController.java).
|
|
Test: [`ValidationScenariosTest.java`](../src/test/java/com/ankurm/customvalidation/ValidationScenariosTest.java),
|
|
[`ProblemDetailsEnabledTest.java`](../src/test/java/com/ankurm/customvalidation/ProblemDetailsEnabledTest.java).
|
|
Transcripts: [`docs/output/03-record-based-spam-rejected.txt`](output/03-record-based-spam-rejected.txt),
|
|
[`docs/output/03b-record-based-spam-rejected-accept-json.txt`](output/03b-record-based-spam-rejected-accept-json.txt),
|
|
[`docs/output/07-problemdetails-enabled-record-rejected.txt`](output/07-problemdetails-enabled-record-rejected.txt).
|
|
|
|
## The surprise this chapter exists to document
|
|
|
|
`ContactController#submitContactForm` (the class-based endpoint) declares a `BindingResult`
|
|
parameter immediately after `@Valid ContactForm`. That is what lets it inspect
|
|
`bindingResult.getFieldErrors()` and hand back a `{"message": "..."}` body of its own construction.
|
|
A record parameter has nowhere convenient to put an equivalent `BindingResult` in this codebase's
|
|
controller signatures, so `submitContactFormRecord` has none -- and a failing constraint on the
|
|
record path throws `MethodArgumentNotValidException` instead of populating a result object.
|
|
|
|
The assumption going in was that Spring's default handling of that exception would still produce
|
|
*some* readable body. It does not, by default:
|
|
|
|
```
|
|
$ curl -s -i -X POST localhost:8080/contact-record \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"email":"[email protected]","message":"This is spam."}'
|
|
|
|
HTTP status: 400
|
|
Body: '' (empty!)
|
|
```
|
|
|
|
([`docs/output/03-record-based-spam-rejected.txt`](output/03-record-based-spam-rejected.txt))
|
|
|
|
<blockquote>An <code>Accept: application/json</code> header does not change this. It was tested
|
|
directly rather than assumed -- see
|
|
<a href="../docs/output/03b-record-based-spam-rejected-accept-json.txt">docs/output/03b-record-based-spam-rejected-accept-json.txt</a>.
|
|
The empty body is not a content-negotiation problem; nothing is being negotiated because nothing is
|
|
being written.</blockquote>
|
|
|
|
## The property that changes this, and what it still does not give you
|
|
|
|
`spring.mvc.problemdetails.enabled=true` (a property that already existed in Spring Boot 3, not
|
|
new in 4.1) turns on RFC 9457 `ProblemDetail` responses for framework-thrown MVC exceptions,
|
|
`MethodArgumentNotValidException` included:
|
|
|
|
```
|
|
HTTP status: 400
|
|
Content-Type: application/problem+json
|
|
Body: {"detail":"Invalid request content.","instance":"/contact-record","status":400,"title":"Bad Request"}
|
|
```
|
|
|
|
([`docs/output/07-problemdetails-enabled-record-rejected.txt`](output/07-problemdetails-enabled-record-rejected.txt))
|
|
|
|
Progress -- there is now a body, and a real HTTP status-coded RFC 9457 document -- but read it
|
|
closely: no mention of "spam", no field name, no constraint message. Boot's default mapping fills
|
|
in only the generic fields (`title`, `status`, a fixed `detail` string). The per-field detail the
|
|
class-based endpoint hand-rolls from `bindingResult.getFieldErrors()` is not reproduced
|
|
automatically; getting it back needs a custom `@ExceptionHandler` (or a
|
|
`ResponseEntityExceptionHandler` override of `handleMethodArgumentNotValid`) that reads the
|
|
exception's `BindingResult` -- the exception itself still carries one, even though the controller
|
|
signature does not expose it -- and writes the field errors into the `ProblemDetail`'s own
|
|
`properties` map.
|
|
|
|
<blockquote>Turning on <code>spring.mvc.problemdetails.enabled</code> is not, by itself, a
|
|
drop-in replacement for the <code>BindingResult</code>-based error reporting pattern most
|
|
Spring tutorials (including the earlier version of this one) teach. It replaces an empty body with
|
|
a standardised envelope; it does not replace the work of putting your validation messages inside
|
|
that envelope.</blockquote>
|
|
|
|
## Going deeper
|
|
|
|
- [Spring Framework `ProblemDetail` reference](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html) (rel="nofollow")
|
|
- [RFC 9457: Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457) (rel="nofollow")
|
|
- See also this repo's [`problem-details/`](../problem-details) module, built for a different
|
|
ankurm.com article specifically about `ProblemDetail` and global exception handling in depth.
|
|
- Prev: [Record validation](02-record-validation.md)
|