Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/99
HTTP/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:

FieldMeaning
statusThe status code
titleA short name of the error
detailDetails for this particular case
instanceThe 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.

Thanks for reading!

Donate to the site — we work hard for you.