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