Add agent research and implementation plan docs for US-1
Research reports on datetime handling, RFC 9457, font selection. Implementation plans for US-1 create event and post-review fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
202
docs/agents/research/2026-03-04-rfc9457-problem-details.md
Normal file
202
docs/agents/research/2026-03-04-rfc9457-problem-details.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
date: 2026-03-04T21:15:50+00:00
|
||||
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
|
||||
branch: master
|
||||
topic: "RFC 9457 Problem Details for HTTP API Error Responses"
|
||||
tags: [research, error-handling, rfc9457, spring-boot, openapi]
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research: RFC 9457 Problem Details
|
||||
|
||||
## Research Question
|
||||
|
||||
How should the fete API structure error responses? What does RFC 9457 (Problem Details) specify, and how does it integrate with Spring Boot 3.5.x, OpenAPI 3.1, and openapi-fetch?
|
||||
|
||||
## Summary
|
||||
|
||||
RFC 9457 (successor to RFC 7807) defines a standard JSON format (`application/problem+json`) for machine-readable HTTP API errors. Spring Boot 3.x has first-class support via `ProblemDetail`, `ErrorResponseException`, and `ResponseEntityExceptionHandler`. The recommended approach is a single `@RestControllerAdvice` that handles all exceptions consistently — no `spring.mvc.problemdetails.enabled` property, no fallback to legacy error format.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### RFC 9457 Format
|
||||
|
||||
Standard fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | URI | Identifies the problem type. Defaults to `about:blank`. |
|
||||
| `title` | string | Short, human-readable summary. Should not change between occurrences. |
|
||||
| `status` | int | HTTP status code. |
|
||||
| `detail` | string | Human-readable explanation specific to this occurrence. |
|
||||
| `instance` | URI | Identifies the specific occurrence (e.g. correlation ID). |
|
||||
|
||||
Extension members (additional JSON properties) are explicitly permitted. This is the mechanism for validation errors, error codes, etc.
|
||||
|
||||
**Key rule:** With `type: "about:blank"`, the `title` must match the HTTP status phrase exactly. Use a custom `type` URI when providing a custom `title`.
|
||||
|
||||
### Spring Boot 3.x Built-in Support
|
||||
|
||||
- **`ProblemDetail`** — container class for the five standard fields + a `properties` Map for extensions.
|
||||
- **`ErrorResponseException`** — base class for custom exceptions that carry their own `ProblemDetail`.
|
||||
- **`ResponseEntityExceptionHandler`** — `@ControllerAdvice` base class that handles all Spring MVC exceptions and renders them as `application/problem+json`.
|
||||
- **`ProblemDetailJacksonMixin`** — automatically unwraps the `properties` Map as top-level JSON fields during serialization.
|
||||
|
||||
### Recommended Configuration
|
||||
|
||||
Use a single `@RestControllerAdvice` extending `ResponseEntityExceptionHandler`. Do **not** use the `spring.mvc.problemdetails.enabled` property.
|
||||
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
// All Spring MVC exceptions are handled automatically.
|
||||
// Add @ExceptionHandler methods for domain exceptions here.
|
||||
// Add a catch-all for Exception.class to prevent legacy error format.
|
||||
}
|
||||
```
|
||||
|
||||
Reasons to avoid the property-based approach:
|
||||
1. No place to add custom `@ExceptionHandler` methods.
|
||||
2. Having both the property AND a custom `ResponseEntityExceptionHandler` bean causes a conflict.
|
||||
3. The property ignores `server.error.include-*` properties.
|
||||
|
||||
### Validation Errors (Field-Level)
|
||||
|
||||
Spring deliberately does **not** include field-level validation errors in `ProblemDetail` by default (security rationale). Override `handleMethodArgumentNotValid`:
|
||||
|
||||
```java
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleMethodArgumentNotValid(
|
||||
MethodArgumentNotValidException ex,
|
||||
HttpHeaders headers,
|
||||
HttpStatusCode status,
|
||||
WebRequest request) {
|
||||
|
||||
ProblemDetail problemDetail = ex.getBody();
|
||||
problemDetail.setTitle("Validation Failed");
|
||||
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
|
||||
|
||||
List<Map<String, String>> fieldErrors = ex.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.map(fe -> Map.of(
|
||||
"field", fe.getField(),
|
||||
"message", fe.getDefaultMessage()
|
||||
))
|
||||
.toList();
|
||||
|
||||
problemDetail.setProperty("fieldErrors", fieldErrors);
|
||||
return handleExceptionInternal(ex, problemDetail, headers, status, request);
|
||||
}
|
||||
```
|
||||
|
||||
Resulting response:
|
||||
```json
|
||||
{
|
||||
"type": "urn:problem-type:validation-error",
|
||||
"title": "Validation Failed",
|
||||
"status": 400,
|
||||
"detail": "Invalid request content.",
|
||||
"instance": "/api/events",
|
||||
"fieldErrors": [
|
||||
{ "field": "title", "message": "must not be blank" },
|
||||
{ "field": "expiryDate", "message": "must be a future date" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI Schema Definition
|
||||
|
||||
```yaml
|
||||
components:
|
||||
schemas:
|
||||
ProblemDetail:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
format: uri
|
||||
default: "about:blank"
|
||||
title:
|
||||
type: string
|
||||
status:
|
||||
type: integer
|
||||
detail:
|
||||
type: string
|
||||
instance:
|
||||
type: string
|
||||
format: uri
|
||||
additionalProperties: true
|
||||
|
||||
ValidationProblemDetail:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ProblemDetail'
|
||||
- type: object
|
||||
properties:
|
||||
fieldErrors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- field
|
||||
- message
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Validation failed
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationProblemDetail'
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetail'
|
||||
```
|
||||
|
||||
Use media type `application/problem+json` in response definitions. Set `additionalProperties: true` on the base schema.
|
||||
|
||||
### Frontend Consumption (openapi-fetch)
|
||||
|
||||
openapi-fetch uses a discriminated union for responses:
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client.POST('/api/events', { body: eventData })
|
||||
|
||||
if (error) {
|
||||
// `error` is typed from the OpenAPI error response schema
|
||||
console.log(error.title) // "Validation Failed"
|
||||
console.log(error.fieldErrors) // [{ field: "title", message: "..." }]
|
||||
return
|
||||
}
|
||||
|
||||
// `data` is the typed success response
|
||||
```
|
||||
|
||||
The `error` object is already typed from the generated schema — no manual type assertions needed for defined error shapes.
|
||||
|
||||
### Known Pitfalls
|
||||
|
||||
| Pitfall | Description | Mitigation |
|
||||
|---------|-------------|------------|
|
||||
| **Inconsistent formats** | Exceptions escaping to Spring Boot's `BasicErrorController` return legacy format (`timestamp`, `error`, `path`), not Problem Details. | Add a catch-all `@ExceptionHandler(Exception.class)` in the `@RestControllerAdvice`. |
|
||||
| **`server.error.include-*` ignored** | When Problem Details is active, these properties have no effect. | Control content via `ProblemDetail` directly. |
|
||||
| **Validation errors hidden by default** | Spring returns only `"Invalid request content."` without field details. | Override `handleMethodArgumentNotValid` explicitly. |
|
||||
| **Content negotiation** | `application/problem+json` is only returned when the client accepts it. `openapi-fetch` sends `Accept: application/json` which Spring considers compatible. | No action needed for SPA clients. |
|
||||
| **`about:blank` semantics** | With `type: "about:blank"`, `title` must match the HTTP status phrase. Custom titles require a custom `type` URI. | Use `urn:problem-type:*` URIs for custom problem types. |
|
||||
|
||||
## Sources
|
||||
|
||||
- [RFC 9457 Full Text](https://www.rfc-editor.org/rfc/rfc9457.html)
|
||||
- [Spring Framework Docs: Error Responses](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html)
|
||||
- [Swagger Blog: Problem Details RFC 9457](https://swagger.io/blog/problem-details-rfc9457-doing-api-errors-well/)
|
||||
- [Baeldung: Returning Errors Using ProblemDetail](https://www.baeldung.com/spring-boot-return-errors-problemdetail)
|
||||
- [SivaLabs: Spring Boot 3 Error Reporting](https://www.sivalabs.in/blog/spring-boot-3-error-reporting-using-problem-details/)
|
||||
- [Spring Boot Issue #43850: Render global errors as Problem Details](https://github.com/spring-projects/spring-boot/issues/43850)
|
||||
Reference in New Issue
Block a user