Intermediate · Harden

Centralize Failures with ProblemDetail

Translate exceptions at one HTTP boundary while preserving useful status, type, title, detail, and correlation data.

Primary source: Spring Boot 3.5: Problem Details Support

35 min

Lesson 3 of 3

1d streak

The Architectural Question

Handler sprawl duplicates mappings across controllers and lets one uncaught exception ship a raw 500 to customers.

What this guide proves

  • Use narrow exception mappings
  • Log unexpected failures once with correlation context
  • Never expose stack traces or persistence implementation details

RFC 9457 Problem Details: The modern API error standard

Prior to Spring Boot 3, every team invented their own ad-hoc JSON error structures (e.g. {"timestamp": ..., "status": 500, "message": "error"}). RFC 9457 (superseding RFC 7807) establishes a universal, machine-readable format for HTTP problem details under the application/problem+json media type.

PropertyRFC 9457 DescriptionExample
typeURI reference identifying the problem typehttps://api.example.com/errors/out-of-stock
titleShort, human-readable summary of problemOut of Stock
statusHTTP status code generated by origin server409
detailHuman-readable explanation specific to this occurrenceItem 801 is out of stock in warehouse US-EAST
instanceURI reference identifying the specific occurrence/orders/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

Centralizing exception translation with @RestControllerAdvice

Canonical error-contract lesson

The Web exception-handling lesson explains MVC resolution mechanics. This lesson owns the application-wide ProblemDetail shape, semantic error codes, and redaction decisions.

@RestControllerAdvice is an interceptor component applied across all controllers. Instead of polluting controllers with try-catch blocks, throw semantic domain exceptions (for example, OrderNotFoundException) and translate them centrally in the advice. For framework validation, handle both MethodArgumentNotValidException for object-parameter validation and HandlerMethodValidationException for method-parameter constraints; Spring MVC can raise either depending on the controller signature.

  1. 01Extend ResponseEntityExceptionHandler to inherit default handling for standard Spring MVC exceptions (e.g. MethodArgumentNotValidException).
  2. 02Define @ExceptionHandler methods for custom application exceptions.
  3. 03Construct and return ProblemDetail instances with status codes and contextual details.
  4. 04Redact all internal database stack traces, SQL strings, and system paths to avoid information disclosure vulnerabilities.

Complete production GlobalExceptionHandler implementation

GlobalExceptionHandler.java
package com.example.common.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.net.URI;
import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(ResourceNotFoundException.class)
    public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
        var problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        problem.setTitle("Resource Not Found");
        problem.setType(URI.create("https://api.example.com/errors/not-found"));
        return problem;
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        problem.setTitle("Validation Failed");
        Map<String, String> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
            .collect(Collectors.toMap(
                fe -> fe.getField(),
                fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid",
                (first, second) -> first));
        problem.setProperty("invalidParams", fieldErrors);
        return ResponseEntity.status(status).body(problem);
    }

    @ExceptionHandler(Exception.class)
    public ProblemDetail handleUnhandled(Exception ex) {
        log.error("Unhandled server exception caught in global advice", ex);
        var problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected internal error occurred.");
        problem.setTitle("Internal Server Error");
        return problem;
    }
}

Break it deliberately: Leaking internal stack traces and SQL syntax

Terminal / Diagnostic Output
Dangerous unredacted response (CWE-209 Information Disclosure):
HTTP/1.1 500 Internal Server Error

{
  "timestamp": "2026-09-03T06:00:00Z",
  "message": "org.postgresql.util.PSQLException: ERROR: relation \"users\" does not exist at character 15",
  "trace": "org.hibernate.exception.SQLGrammarException at com.example.user.UserService.find(UserService.java:42)..."
}

Secure sanitized response:
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json

{
  "type": "about:blank",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "An unexpected internal error occurred."
}

Active Recall Checkpoint

Why should controller methods never catch exceptions and return custom error maps manually?

Hint: Consider code duplication and consistency across the API.

Show answer & explanation

Manual try-catch blocks in controllers lead to duplicated error-handling logic, inconsistent status codes, and risk leaking sensitive exceptions. Centralizing in @RestControllerAdvice guarantees uniform RFC 9457 schemas and clean controllers.

Best practices for production error contracts

Use it when

Set spring.mvc.problemdetails.enabled=true when relying on Boot's built-in handling, then adopt RFC 9457 ProblemDetail via @RestControllerAdvice as the application error boundary.

Reconsider when

Never expose internal exception class names, database connection errors, or stack traces in HTTP response payloads.

Working Implementation

Code Walkthrough

Create one `@RestControllerAdvice` and snapshot-test its error payloads.

ProblemDetailsLab.java
package com.example.problemdetails;

import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.net.URI;
import java.time.Instant;

@RestControllerAdvice
public class ProblemDetailsLab {

    public static class ResourceNotFoundException extends RuntimeException {
        public ResourceNotFoundException(String message) { super(message); }
    }

    // RFC 9457 Problem Details response
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ProblemDetail> handleNotFound(ResourceNotFoundException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        problem.setTitle("Resource Not Found");
        problem.setType(URI.create("https://api.example.com/errors/not-found"));
        problem.setProperty("timestamp", Instant.now());
        problem.setProperty("errorCode", "ERR_RESOURCE_MISSING");
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problem);
    }
}
Spring Framework 6 represents RFC 9457 Problem Details with the ProblemDetail class; Boot can enable or customize the MVC error-handling integration.
ProblemDetail standardizes error fields: type, title, status, detail, instance.
Use problem.setProperty(key, value) to append custom error extensions in a backwards-compatible manner.

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Catch Exception broadly in a single advice method
  2. Observe: Programming errors masquerade as 400s and correlation identifiers disappear from the logs
  3. Repair: Narrow mappings per exception family plus one logged fallback keep status codes truthful and incidents traceable.

Production Acceptance Criteria

  • Map three specific exceptions to three distinct statuses
  • Log unknown failures exactly once with correlation context
  • Hide stack traces while keeping detail messages useful

Architectural & Technical Review

Core Interview Questions

Q01What are the core properties defined by RFC 9457 Problem Details?Show answer

type (URI identifier), title (human-readable summary), status (HTTP status code), detail (specific occurrence explanation), and instance (URI).

Q02How does @RestControllerAdvice improve controller maintainability?Show answer

It eliminates repetitive try-catch blocks in controllers by intercepting thrown domain exceptions and mapping them to HTTP responses globally.

Q03Why must internal database exceptions be redacted before sending to clients?Show answer

Leaking SQL statements, table names, or stack traces creates CWE-209 information disclosure vulnerabilities that assist attackers.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Catching Exception and returning generic 500 without logging — Swallowing unhandled exceptions without logging stack traces prevents ops teams from diagnosing server faults.
  • Pitfall: Returning ad-hoc error maps instead of ProblemDetail — Inconsistent JSON error structures increase integration friction for API consumers; adopt RFC 9457 uniformly.

Module Certification Quiz

Prove Module Mastery

Comprehensive question set covering all lessons in this module.

Take Module Quiz

Primary Documentation & Specifications