Error handling with Spring WebFlux

Posted at — Apr 11, 2022
Riekpil logo
Learn how to test real-world applications with the Testing Spring Boot Applications Masterclass. Comprehensive online course with 8 modules and 130+ video lessons to master well-known Java testing libraries: JUnit 5, Mockito, Testcontainers, WireMock, Awaitility, Selenium, LocalStack, Selenide, and Spring's Outstanding Test Support.

My error-handling-spring-boot-starter library had its first release almost 2 years ago. The first reported issue came soon after with a request to support Spring WebFlux. As I don’t have any Spring WebFlux projects myself, it was pretty low on my priority list. Especially as it did not seem trivial to add support.

Luckily, about a month ago Fabio Marini opened a PR with all the building blocks I needed to add support for Spring WebFlux to the library.

Version 3.0.0 has now been released which can be used with Spring WebFlux.

Had to do a small bugfix for the @WebFluxTest support so the current version is now 3.0.1 (I should have done the release blog entry before the release :-) )

To get started, create a Spring Boot application at https://start.spring.io and select 'Spring Reactive Web' from the dependencies list. Optionally, also select 'Validation' if you want to use the validation annotations.

Add the error-handling-spring-boot-starter library in the pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    ...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.wimdeblauwe</groupId>
            <artifactId>error-handling-spring-boot-starter</artifactId>
            <version>3.0.1</version>
        </dependency>
        ...
    </dependencies>
</project>

Now we can build a simple rest controller:

@RestController
@RequestMapping("/")
public class MyRestController {

    private final UserService service;

    public MyRestController(UserService service) {
        this.service = service;
    }

    @GetMapping("/users/{id}")
    Mono<UserDto> findUser(@PathVariable("id") Long id) {
        return service.findUserById(id)
                      .map(user -> new UserDto(user.name()));
    }
}

With UserService:

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@Service
public class UserService {

    private final Map<Long, User> users = new HashMap<>();

    public UserService() {
        users.put(1L, new User("Wim"));
        users.put(2L, new User("Simon"));
        users.put(3L, new User("Siva"));
        users.put(4L, new User("Josh"));
    }

    public Mono<User> findUserById(Long userId) {
        User user = users.get(userId);
        if (user == null) {
            throw new UserNotFoundException(userId);
        }
        return Mono.just(user);
    }
}

User and UserDto are just 2 simple records:

public record User(String name) {
}

public record UserDto(String name) {

}

Now run the application and try the endpoint with your favorite HTTP client (Mine is the one from IntelliJ IDEA):

GET localhost:8080/users/1

Response:

{
  "name": "Wim"
}

If we use an id that does not exist:

GET localhost:8080/users/10

Then we get this error response:

{
  "code": "USER_NOT_FOUND",
  "message": "No user found for id 10",
  "userId": 10
}

The userId property is added in the JSON resonse because we used the @ResponseErrorProperty annotation in the exception class:

import io.github.wimdeblauwe.errorhandlingspringbootstarter.ResponseErrorProperty;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
    private final Long userId;

    public UserNotFoundException(Long userId) {
        super("No user found for id " + userId);
        this.userId = userId;
    }

    @ResponseErrorProperty
    public Long getUserId() {
        return userId;
    }
}

We can also validate this with the following @WebFluxTest test:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.reactive.server.WebTestClient;

@WebFluxTest(MyRestController.class)
@Import(UserService.class)
class MyRestControllerTest {

    @Autowired
    WebTestClient webTestClient;

    @Test
    void testUserNotFound() {
        webTestClient.get()
                     .uri("/users/10")
                     .exchange()
                     .expectStatus().isNotFound()
                     .expectBody()
                     .consumeWith(System.out::println)
                     .jsonPath("$.code").isEqualTo("USER_NOT_FOUND")
                     .jsonPath("$.message").isEqualTo("No user found for id 10")
                     .jsonPath("$.userId").isEqualTo(10L);

    }
}

Conclusion

The error-handling-spring-boot-starter library is fully ready for Spring WebFlux.

See reactive-error-handling on GitHub for the full sources of this example.

If you have any questions or remarks, feel free to post a comment at GitHub discussions.

If you want to be notified in the future about new articles, as well as other interesting things I'm working on, join my mailing list!
I send emails quite infrequently, and will never share your email address with anyone else.