- Move cross-cutting docs (personas, design system, implementation phases, Ideen.md) to .specify/memory/ - Move cross-cutting research and plans to .specify/memory/research/ and .specify/memory/plans/ - Extract 5 setup tasks from spec/setup-tasks.md into individual specs/001-005/spec.md files with spec-kit template format - Extract 20 user stories from spec/userstories.md into individual specs/006-026/spec.md files with spec-kit template format - Relocate feature-specific research and plan docs into specs/[feature]/ - Add spec-kit constitution, templates, scripts, and slash commands - Slim down CLAUDE.md to Claude-Code-specific config, delegate principles to .specify/memory/constitution.md - Update ralph.sh with stream-json output and per-iteration logging - Delete old spec/ and docs/agents/ directories - Gitignore Ralph iteration JSONL logs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
8.0 KiB
Markdown
203 lines
8.0 KiB
Markdown
---
|
|
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)
|