1. Overview
In this tutorial, we’re going to explore how to produce application/problem+json responses using the Problem Spring Web library. This library helps us to avoid repetitive tasks related to error handling.
By integrating Problem Spring Web into our Spring Boot application, we can simplify the way we handle exceptions within our project and generate responses accordingly.
2. The Problem Library
Problem is a small library with the purpose of standardizing the way Java-based Rest APIs express errors to their consumers.
A Problem is an abstraction of any error we want to inform about. It contains handy information about the error. Let’s see the default representation of a Problem response:
{ "title": "Not Found", "status": 404 }
In this case, the status code and the title are enough to describe the error. However, we can also add a detailed description of it:
{ "title": "Service Unavailable", "status": 503, "detail": "Database not reachable" }
We can also create custom Problem objects that adapt to our needs:
Problem.builder() .withType(URI.create("https://example.org/out-of-stock")) .withTitle("Out of Stock") .withStatus(BAD_REQUEST) .withDetail("Item B00027Y5QG is no longer available") .with("product", "B00027Y5QG") .build();
In this tutorial we’ll focus on the Problem library implementation for Spring Boot projects.
3. Problem Spring Web Setup
Since this is a Maven based project, let’s add the problem-spring-web dependency to the pom.xml:
<dependency> <groupId>org.zalando</groupId> <artifactId>problem-spring-web</artifactId> <version>0.23.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.12.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.12.RELEASE</version> </dependency>
We also need the spring-boot-starter-web and the spring-boot-starter-security dependencies. Spring Security is required from version 0.23.0 of problem-spring-web.
4. Basic Configuration
As our first step, we need to disable the white label error page so we’ll able to see our custom error representation instead:
@EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)
Now, let’s register some of the required components in the ObjectMapper bean:
@Bean public ObjectMapper objectMapper() { return new ObjectMapper().registerModules( new ProblemModule(), new ConstraintViolationProblemModule()); }
After that, we need to add the following properties to the application.properties file:
spring.resources.add-mappings=false spring.mvc.throw-exception-if-no-handler-found=true spring.http.encoding.force=true
And finally, we need to implement the ProblemHandling interface:
@ControllerAdvice public class ExceptionHandler implements ProblemHandling {}
5. Advanced Configuration
In addition to the basic configuration, we can also configure our project to handle security-related problems. The first step is to create a configuration class to enable the library integration with Spring Security:
@Configuration @EnableWebSecurity @Import(SecurityProblemSupport.class) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private SecurityProblemSupport problemSupport; @Override protected void configure(HttpSecurity http) throws Exception { // Other security-related configuration http.exceptionHandling() .authenticationEntryPoint(problemSupport) .accessDeniedHandler(problemSupport); } }
And finally, we need to create an exception handler for security-related exceptions:
@ControllerAdvice public class SecurityExceptionHandler implements SecurityAdviceTrait {}
6. The REST Controller
After configuring our application, we are ready to create a RESTful controller:
@RestController @RequestMapping("/tasks") public class ProblemDemoController { private static final Map<Long, Task> MY_TASKS; static { MY_TASKS = new HashMap<>(); MY_TASKS.put(1L, new Task(1L, "My first task")); MY_TASKS.put(2L, new Task(2L, "My second task")); } @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List<Task> getTasks() { return new ArrayList<>(MY_TASKS.values()); } @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) public Task getTasks(@PathVariable("id") Long taskId) { if (MY_TASKS.containsKey(taskId)) { return MY_TASKS.get(taskId); } else { throw new TaskNotFoundProblem(taskId); } } @PutMapping("/{id}") public void updateTask(@PathVariable("id") Long id) { throw new UnsupportedOperationException(); } @DeleteMapping("/{id}") public void deleteTask(@PathVariable("id") Long id) { throw new AccessDeniedException("You can't delete this task"); } }
In this controller, we’re intentionally throwing some exceptions. Those exceptions will be converted into Problem objects automatically to produce an application/problem+json response with the details of the failure.
Now, let’s talk about the built-in advice traits and also how to create a custom Problem implementation.
7. Built-in Advice Traits
An advice trait is a small exception handler that catches exceptions and returns the proper problem object.
There are built-in advice traits for common exceptions. Hence, we can use them by simply throwing the exception:
throw new UnsupportedOperationException();
As a result, we’ll get the response:
{ "title": "Not Implemented", "status": 501 }
Since we configured the integration with Spring Security as well, we’re able to throw security-related exceptions:
throw new AccessDeniedException("You can't delete this task");
And get the proper response:
{ "title": "Forbidden", "status": 403, "detail": "You can't delete this task" }
8. Creating a Custom Problem
It’s possible to create a custom implementation of a Problem. We just need to extend the AbstractThrowableProblem class:
public class TaskNotFoundProblem extends AbstractThrowableProblem { private static final URI TYPE = URI.create("https://example.org/not-found"); public TaskNotFoundProblem(Long taskId) { super( TYPE, "Not found", Status.NOT_FOUND, String.format("Task '%s' not found", taskId)); } }
And we can throw our custom problem as follows:
if (MY_TASKS.containsKey(taskId)) { return MY_TASKS.get(taskId); } else { throw new TaskNotFoundProblem(taskId); }
As a result of throwing the TaskNotFoundProblem problem, we’ll get:
{ "type": "https://example.org/not-found", "title": "Not found", "status": 404, "detail": "Task '3' not found" }
9. Dealing with Stack Traces
If we want to include stack traces within the response, we need to configure our ProblemModule accordingly:
ObjectMapper mapper = new ObjectMapper() .registerModule(new ProblemModule().withStackTraces());
The causal chain of causes is disabled by default, but we can easily enable it by overriding the behavior:
@ControllerAdvice class ExceptionHandling implements ProblemHandling { @Override public boolean isCausalChainsEnabled() { return true; } }
After enabling both features we’ll get a response similar to this one:
{ "title": "Internal Server Error", "status": 500, "detail": "Illegal State", "stacktrace": [ "org.example.ExampleRestController .newIllegalState(ExampleRestController.java:96)", "org.example.ExampleRestController .nestedThrowable(ExampleRestController.java:91)" ], "cause": { "title": "Internal Server Error", "status": 500, "detail": "Illegal Argument", "stacktrace": [ "org.example.ExampleRestController .newIllegalArgument(ExampleRestController.java:100)", "org.example.ExampleRestController .nestedThrowable(ExampleRestController.java:88)" ], "cause": { // .... } } }
10. Conclusion
In this article, we explored how to use the Problem Spring Web library to create responses with the errors’ details using an application/problem+json response. We also learned how to configure the library in our Spring Boot application and create a custom implementation of a Problem object.
The implementation of this guide can be found in the GitHub project – this is a Maven based project, so it should be easy to import and run it as is.