Error handling and ProblemDetail
Right now a request for a missing post ends with a 500 error, and the response to invalid data does not say which field failed. A good API answers errors as carefully as successful requests. Spring has exception handlers for this, and the ProblemDetail class — a standard format for describing an error (RFC 9457).
A handler class
Create a class that catches exceptions from all controllers:
// src/main/java/com/example/blog/ApiExceptionHandler.java
package com.example.blog;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(PostNotFoundException.class)
public ProblemDetail handleNotFound(PostNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setTitle("Post not found");
return problem;
}
}@RestControllerAdvice makes the class a shared helper for all controllers. The method with @ExceptionHandler(PostNotFoundException.class) is called when any controller throws that exception, and its result goes to the client instead of a 500 error.
What the client gets
curl -i http://localhost:8080/api/posts/99HTTP/1.1 404
Content-Type: application/problem+json
{"detail":"Post 99 not found","instance":"/api/posts/99","status":404,"title":"Post not found"}The application/problem+json type tells the client this is an error description. The fields are standard:
| Field | Meaning |
|---|---|
status | The status code |
title | A short name of the error |
detail | Details for this particular case |
instance | The request address where the error happened |
Validation errors per field
Let's add a second method to the same class — for the exception Spring throws when a @Valid check fails:
// src/main/java/com/example/blog/ApiExceptionHandler.java
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new TreeMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
problem.setProperty("errors", errors);
return problem;
}getFieldErrors() returns a list of errors with the field name and the message. setProperty adds our own errors field to the response. TreeMap sorts the fields alphabetically so the response is stable.
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"","body":"x"}'{"detail":"Validation failed","instance":"/api/posts","status":400,"title":"Bad Request","errors":{"author":"must not be blank","title":"must not be blank"}}Now the client sees that two fields need fixing.
Common mistake
Spring Boot has a setting spring.mvc.problemdetails.enabled: true — it turns all of Spring's standard errors into the ProblemDetail format. But it also switches on a built-in handler, and that handler catches MethodArgumentNotValidException before your method does. The response again has no list of fields:
{"detail":"Invalid request content.","instance":"/api/posts","status":400,"title":"Bad Request"}Choose one: either your own handling of validation errors, as in this lesson, or the setting. If you need both, make the handler class extend ResponseEntityExceptionHandler and override its handleMethodArgumentNotValid method.