WebFlux
Reactive Programming
Spring Framework
Flux
HTTP 404

WebFlux functional How to detect an empty Flux and return 404?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Spring WebFlux, a Flux that completes without emitting any elements typically results in an empty 200 response. To return a 404 instead, you need to detect the empty case and switch the response. The main approaches are switchIfEmpty(), collectList() with a check, and hasElements(). For Mono, use switchIfEmpty(Mono.error(...)) to throw a ResponseStatusException. The choice depends on whether you are working with functional endpoints or annotated controllers.

Functional Endpoint: switchIfEmpty

java
1import org.springframework.web.reactive.function.server.*;
2import reactor.core.publisher.Flux;
3import reactor.core.publisher.Mono;
4
5public class UserHandler {
6
7    private final UserRepository repository;
8
9    public Mono<ServerResponse> getUsers(ServerRequest request) {
10        String city = request.queryParam("city").orElse("");
11
12        Flux<User> users = repository.findByCity(city);
13
14        return users
15            .collectList()
16            .flatMap(list -> {
17                if (list.isEmpty()) {
18                    return ServerResponse.notFound().build();
19                }
20                return ServerResponse.ok().bodyValue(list);
21            });
22    }
23}

collectList() gathers all elements into a List, which you can then check for emptiness. This is the simplest approach but buffers all elements in memory.

Using switchIfEmpty with Flux

java
1public Mono<ServerResponse> getUsersOptimized(ServerRequest request) {
2    String city = request.queryParam("city").orElse("");
3
4    Flux<User> users = repository.findByCity(city);
5
6    return ServerResponse.ok()
7        .contentType(MediaType.APPLICATION_JSON)
8        .body(users, User.class)
9        .switchIfEmpty(ServerResponse.notFound().build());
10}

This approach streams results without buffering. However, switchIfEmpty on ServerResponse works because ServerResponse.ok().body(...) returns a Mono<ServerResponse> that completes empty when the Flux is empty.

Annotated Controller with Mono

java
1import org.springframework.web.bind.annotation.*;
2import org.springframework.web.server.ResponseStatusException;
3import org.springframework.http.HttpStatus;
4import reactor.core.publisher.Mono;
5
6@RestController
7@RequestMapping("/api/users")
8public class UserController {
9
10    private final UserRepository repository;
11
12    @GetMapping("/{id}")
13    public Mono<User> getUser(@PathVariable String id) {
14        return repository.findById(id)
15            .switchIfEmpty(Mono.error(
16                new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
17            ));
18    }
19}

For Mono (single result), switchIfEmpty(Mono.error(...)) with ResponseStatusException is the cleanest pattern. Spring automatically converts the exception to a 404 response.

Annotated Controller with Flux

java
1@GetMapping
2public Mono<ResponseEntity<List<User>>> getUsers(@RequestParam String city) {
3    return repository.findByCity(city)
4        .collectList()
5        .map(users -> {
6            if (users.isEmpty()) {
7                return ResponseEntity.notFound().<List<User>>build();
8            }
9            return ResponseEntity.ok(users);
10        });
11}

Wrapping in ResponseEntity gives explicit control over the HTTP status code. The collectList() + conditional pattern works for both Flux and streaming scenarios.

Using hasElements()

java
1public Mono<ServerResponse> getUsersHasElements(ServerRequest request) {
2    Flux<User> users = repository.findAll();
3
4    return users.hasElements()
5        .flatMap(hasElements -> {
6            if (!hasElements) {
7                return ServerResponse.notFound().build();
8            }
9            // Re-query because hasElements consumed the Flux
10            return ServerResponse.ok().body(repository.findAll(), User.class);
11        });
12}

hasElements() returns Mono<Boolean> indicating whether the Flux emitted at least one element. The downside is that it subscribes to the Flux, so you must re-query or cache the results.

Using Flux.next() for First Element Check

java
1public Mono<ServerResponse> getUsersNext(ServerRequest request) {
2    Flux<User> users = repository.findByCity("NYC");
3
4    return users
5        .next()  // Get first element as Mono
6        .flatMap(firstUser ->
7            ServerResponse.ok().body(repository.findByCity("NYC"), User.class)
8        )
9        .switchIfEmpty(ServerResponse.notFound().build());
10}

.next() takes the first element from the Flux. If empty, it returns an empty Mono, which triggers switchIfEmpty. Again, the re-query is a downside.

Caching with Flux.cache()

java
1public Mono<ServerResponse> getUsersCached(ServerRequest request) {
2    Flux<User> users = repository.findByCity("NYC").cache();
3
4    return users
5        .hasElements()
6        .flatMap(hasElements -> {
7            if (!hasElements) {
8                return ServerResponse.notFound().build();
9            }
10            // users.cache() allows resubscription without re-querying
11            return ServerResponse.ok().body(users, User.class);
12        });
13}

.cache() replays the Flux for subsequent subscribers, avoiding the re-query problem. Use it when the data is small enough to fit in memory.

Global Exception Handler

java
1@ControllerAdvice
2public class GlobalExceptionHandler {
3
4    @ExceptionHandler(ResponseStatusException.class)
5    public Mono<ResponseEntity<Map<String, String>>> handleNotFound(ResponseStatusException ex) {
6        Map<String, String> body = Map.of(
7            "error", ex.getReason() != null ? ex.getReason() : "Not found",
8            "status", String.valueOf(ex.getStatusCode().value())
9        );
10        return Mono.just(ResponseEntity.status(ex.getStatusCode()).body(body));
11    }
12}

A @ControllerAdvice handler catches ResponseStatusException thrown by switchIfEmpty(Mono.error(...)) and formats a consistent error response.

Common Pitfalls

  • Returning an empty Flux produces 200, not 404: WebFlux returns 200 with an empty JSON array [] by default. You must explicitly check for empty and return 404 if that is the desired behavior.
  • hasElements() consumes the Flux: After calling hasElements(), the original Flux is consumed. You must re-query the data or use .cache() to replay it. Forgetting this returns an empty response even when data exists.
  • collectList() buffers everything in memory: For large result sets, collectList() loads all records into a List, defeating the purpose of reactive streaming. Use it only when the result set is bounded and small.
  • switchIfEmpty with a Mono.defer: switchIfEmpty(ServerResponse.notFound().build()) evaluates eagerly. If the 404 response construction has side effects, wrap it in Mono.defer(() -> ServerResponse.notFound().build()) for lazy evaluation.
  • Mixing blocking and reactive code: Calling .block() on a Mono/Flux inside a WebFlux handler causes IllegalStateException. Use .flatMap, .map, and switchIfEmpty to keep the entire chain reactive.

Summary

  • Use switchIfEmpty(Mono.error(new ResponseStatusException(404))) for Mono endpoints
  • Use collectList() + conditional for Flux endpoints when buffering is acceptable
  • Use .cache() with hasElements() to avoid re-querying the database
  • ResponseEntity gives explicit control over HTTP status codes in annotated controllers
  • Functional endpoints use ServerResponse.notFound().build() for 404 responses
  • An empty Flux returns 200 with [] by default — always handle the empty case explicitly

Course illustration
Course illustration

All Rights Reserved.