1. Overview

In this article, we’ll explore using ProblemDetail to return errors in Spring Boot applications. Whether we’re handling REST APIs or reactive streams, it offers a standardized way to communicate errors to clients.

Let’s dive into why we’d care about it. We’ll explore how error handling was done before its introduction, then, we’ll also discuss the specifications behind this powerful tool. Finally, we’ll learn how to prepare error responses using it.

2. Why Should We Care About ProblemDetail?

Using ProblemDetail to standardize error responses is crucial for any API.

It helps clients understand and handle errors, improving the API’s usability and debuggability. This leads to a better developer experience and more robust applications.

Adopting it can also help provide more informative error messages that are essential for maintaining and troubleshooting our services.

3. Traditional Error Handling Approaches

Before ProblemDetail, we often implemented custom exception handlers and response entities to handle errors in Spring Boot. We’d create custom error response structures. That resulted in inconsistencies across different APIs.

Also, this approach required a lot of boilerplate code. Moreover, it lacked a standardized way to represent errors, making it difficult for clients to parse and understand error messages uniformly.

4. ProblemDetail Specification

The ProblemDetail specification is part of the RFC 7807 standard.  It defines a consistent structure for error responses, including fields like type, title, status, detail, and instance. This standardization helps API developers and consumers by providing a common format for error information.

Implementing ProblemDetail ensures that our error responses are predictable and easy to understand. That in turn improves overall communication between our API and its clients.

Next, we’ll look at implementing it in our Spring Boot application, starting with basic setup and configuration.

5. Implementing ProblemDetail in Spring Boot

There are multiple ways to implement problem details in Spring Boot.

5.1. Enabling ProblemDetail Using Application Property

First, we can add a property to enable it. For RESTful service, we add the following property to application.properties:

spring.mvc.problemdetails.enabled=true

This property enables the automatic use of ProblemDetail for error handling in MVC (servlet stack) based applications.

For reactive applications, we’d add the following property:

spring.webflux.problemdetails.enabled=true

Once enabled, Spring reports errors using ProblemDetail:

{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "detail": "Invalid request content.",
    "instance": "/sales/calculate"
}

This property provides ProblemDetail automatically in error handling. Also, we can turn it off if it’s not needed.

5.2. Implementing ProblemDetail in Exception Handler

Global exception handlers implement centralized error handling in the Spring Boot REST applications.

Let’s consider a simple REST service to calculate discounted prices.

It takes an operation request and returns the result. Additionally, it also performs input validation and enforces business rules.

Let’s see the implementation of the request:

public record OperationRequest(
    @NotNull(message = "Base price should be greater than zero.")
    @Positive(message = "Base price should be greater than zero.")
        Double basePrice,
    @Nullable @Positive(message = "Discount should be greater than zero when provided.")
        Double discount) {}

Here is the implementation of the result:

public record OperationResult(
    @Positive(message = "Base price should be greater than zero.") Double basePrice,
    @Nullable @Positive(message = "Discount should be greater than zero when provided.")
        Double discount,
    @Nullable @Positive(message = "Selling price should be greater than zero.")
        Double sellingPrice) {}

And, here’s the implementation of the invalid operation exception:

public class InvalidInputException extends RuntimeException {

    public InvalidInputException(String s) {
        super(s);
    }
}

Now, let’s implement the REST controller to serve the endpoint:

@RestController
@RequestMapping("sales")
public class SalesController {

    @PostMapping("/calculate")
    public ResponseEntity<OperationResult> calculate(
        @Validated @RequestBody OperationRequest operationRequest) {
    
        OperationResult operationResult = null;
        Double discount = operationRequest.discount();
        if (discount == null) {
            operationResult =
                new OperationResult(operationRequest.basePrice(), null, operationRequest.basePrice());
        } else {
            if (discount.intValue() >= 100) {
                throw new InvalidInputException("Free sale is not allowed.");
            } else if (discount.intValue() > 30) {
                throw new IllegalArgumentException("Discount greater than 30% not allowed.");
            } else {
                operationResult = new OperationResult(operationRequest.basePrice(),
                    discount,
                    operationRequest.basePrice() * (100 - discount) / 100);
            }
        }
        return ResponseEntity.ok(operationResult);
    }
}

The SalesController class processes HTTP POST requests at the “/sales/calculate” endpoint.

It checks and validates an OperationRequest object. If the request is valid, it calculates the sale price, considering an optional discount. It throws exceptions if the discount is invalid (more than 100% or more than 30%). If the discount is valid, it calculates the final price by applying it and returns an OperationResult wrapped in a ResponseEntity.

Let’s now see how to implement ProblemDetail in the global exception handler:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(InvalidInputException.class)
    public ProblemDetail handleInvalidInputException(InvalidInputException e, WebRequest request) {
        ProblemDetail problemDetail
            = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
        problemDetail.setInstance(URI.create("discount"));
        return problemDetail;
    }
}

The GlobalExceptionHandler class, annotated with @RestControllerAdvice, extends ResponseEntityExceptionHandler to provide centralized exception handling in a Spring Boot application.

It defines a method to handle InvalidInputException exceptions. When this exception occurs, it creates a ProblemDetail object with a BAD_REQUEST status and the exception’s message. Also, it sets the instance to a URI (“discount”) to indicate the specific context of the error.

This standardized error response provides clear and detailed information to the client about what went wrong.

The ResponseEntityExceptionHandler is a class that is convenient for handling exceptions in a standardized way across applications. Thus, the process of converting exceptions into meaningful HTTP responses is simplified. Moreover, it provides methods to handle common Spring MVC exceptions like MissingServletRequestParameterException, MethodArgumentNotValidException, etc out of the box using ProblemDetail.

5.3. Testing ProblemDetail Implementation

Let’s now test our functionality:

@Test
void givenFreeSale_whenSellingPriceIsCalculated_thenReturnError() throws Exception {

    OperationRequest operationRequest = new OperationRequest(100.0, 140.0);
    mockMvc
      .perform(MockMvcRequestBuilders.post("/sales/calculate")
      .content(toJson(operationRequest))
      .contentType(MediaType.APPLICATION_JSON))
      .andDo(print())
      .andExpectAll(status().isBadRequest(),
        jsonPath("$.title").value(HttpStatus.BAD_REQUEST.getReasonPhrase()),
        jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()),
        jsonPath("$.detail").value("Free sale is not allowed."),
        jsonPath("$.instance").value("discount"))
      .andReturn();
}

In this SalesControllerUnitTest, we’ve autowired the MockMvc and ObjectMapper for testing the SalesController.

The test method givenFreeSale_whenSellingPriceIsCalculated_thenReturnError() simulates a POST request to the “/sales/calculate” endpoint with an OperationRequest containing a base price of 100.0 and a discount of 140.0. So, this should trigger the InvalidOperandException in the controller.

Finally, we verify the response of type BadRequest with a ProblemDetail indicating that “Free sale is not allowed.”

6. Conclusion

In this tutorial, we explored ProblemDetails, its specification, and its implementation in a Spring Boot REST application. Then, we discussed the advantages over traditional error handling, and how to use it in servlet and reactive stacks.

As always, the source code is available over on GitHub.