Files
fete/.specify/memory/research/rfc9457-problem-details.md
nitrix 6aeb4b8bca Migrate project artifacts to spec-kit format
- 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>
2026-03-06 20:19:41 +01:00

8.0 KiB

date, git_commit, branch, topic, tags, status
date git_commit branch topic tags status
2026-03-04T21:15:50+00:00 b8421274b4 master RFC 9457 Problem Details for HTTP API Error Responses
research
error-handling
rfc9457
spring-boot
openapi
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.

Use a single @RestControllerAdvice extending ResponseEntityExceptionHandler. Do not use the spring.mvc.problemdetails.enabled property.

@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:

@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:

{
  "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

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:

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