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
Lesson 3 of 3
1d streakThe 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.
| Property | RFC 9457 Description | Example |
|---|---|---|
| type | URI reference identifying the problem type | https://api.example.com/errors/out-of-stock |
| title | Short, human-readable summary of problem | Out of Stock |
| status | HTTP status code generated by origin server | 409 |
| detail | Human-readable explanation specific to this occurrence | Item 801 is out of stock in warehouse US-EAST |
| instance | URI reference identifying the specific occurrence | /orders/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d |
Centralizing exception translation with @RestControllerAdvice
Canonical error-contract lesson
@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.
- 01Extend ResponseEntityExceptionHandler to inherit default handling for standard Spring MVC exceptions (e.g. MethodArgumentNotValidException).
- 02Define @ExceptionHandler methods for custom application exceptions.
- 03Construct and return ProblemDetail instances with status codes and contextual details.
- 04Redact all internal database stack traces, SQL strings, and system paths to avoid information disclosure vulnerabilities.
Complete production GlobalExceptionHandler implementation
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
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.
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);
}
}Synthesis & Production Verification
Deliberate Failure Condition
- Break: Catch Exception broadly in a single advice method
- Observe: Programming errors masquerade as 400s and correlation identifiers disappear from the logs
- 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
Key Takeaway
@RestControllerAdvice centralizes exception translation across all controllers, formatting errors into standardized RFC 9457 ProblemDetail documents.
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.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources