--- 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 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> 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)