Standard error responses with ProblemDetail

Spring Boot · lesson 38 of 39 · 4 min read

RFC 7807, so every service in the estate returns errors in the same shape.

Open this lesson in the learning hub

Key points

  • Hand-rolled error bodies drift. One service returns message, another error, a third a bare string - and every client writes its own parsing.
  • RFC 7807 defines application/problem+json with a fixed core: type, title, status, detail and instance, plus any extra fields you add.
  • Spring Framework 6 ships ProblemDetail and can produce it for built-in exceptions - set spring.mvc.problemdetails.enabled=true and validation and 404 responses adopt the format.
  • Extending ResponseEntityExceptionHandler gives you the built-in handling and lets you override individual exceptions rather than reimplementing all of them.
  • Put a stable, documented URI in type. That is the field clients branch on - never the human-readable title, which you will want to reword.
  • Keep internal detail out of detail. A stack trace or SQL fragment in an error body is an information leak, and it will end up in a customer log.

Example

@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(InsufficientFundsException.class)
    ProblemDetail handleFunds(InsufficientFundsException ex) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(
                HttpStatus.CONFLICT, "Balance is lower than the requested amount.");

        // Stable and documented - this is what clients switch on.
        pd.setType(URI.create("https://api.example.com/problems/insufficient-funds"));
        pd.setTitle("Insufficient funds");

        // Extra members are allowed and are where the useful context goes.
        pd.setProperty("accountId", ex.accountId());
        pd.setProperty("shortfall", ex.shortfall());
        pd.setProperty("traceId", MDC.get("traceId"));
        return pd;
    }
}

/*
 * HTTP/1.1 409 Conflict
 * Content-Type: application/problem+json
 *
 * {
 *   "type":     "https://api.example.com/problems/insufficient-funds",
 *   "title":    "Insufficient funds",
 *   "status":   409,
 *   "detail":   "Balance is lower than the requested amount.",
 *   "instance": "/accounts/42/withdrawals",
 *   "accountId": 42,
 *   "shortfall": 25.50,
 *   "traceId":  "8f3c1a..."
 * }
 */

One error shape across every service, with a stable type URI clients can branch on and no internals leaked in the body.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Boot course, and every lesson in it is listed on the Spring Boot contents page.