First, a reactor operator can ends in different ways:
- Completes successfully after emitting one (Mono) or more (Flux) value(s)
- Completes empty: The pipeline sends completion signal, but no value has been emitted
- Completes in error: somewhere in the pipeline, an error happened. By default, as in imperative code, it stops the chain of operations, and is propagated.
- Cancelled: the pipeline might be interrupted by a manual action or a system shutdown. It then ends in error (a special kind of error, but an error nonetheless)
Secondly, reactive-stream, whatever the implementation (RxJava or Reactor) does not accept null values. It means that trying to produce a null value in/from a reactive stream will either cause an error or an undefined behavior. This is stated in reactive-stream specification, rule 2.13:
Calling [...] onNext [...] MUST return normally except when any provided parameter is null in which case it MUST throw a java.lang.NullPointerException to the caller
Let's try to produce some simple examples first.
This program shows the possible ways a pipeline can complete:
// Reactive streams does not accept null values:
try {
Mono.just(null);
} catch (NullPointerException e) {
System.out.println("NULL VALUE NOT ACCEPTED !");
}
// Mono/Flux operations stop if an error occurs internally, and send it downstream
try {
Mono.just("Something")
.map(it -> { throw new IllegalStateException("Bouh !"); })
.block();
} catch (IllegalStateException e) {
System.out.println("Error propagated: "+e.getMessage());
}
// A mono or a flux can end "empty". It means that no value or error happened.
// The operation just finished without any result
var result = Mono.just("Hello !")
.filter(it -> !it.endsWith("!"))
// Materialize allow to receive the type of signal produced by the pipeline (next value, error, completion, etc.)
.materialize()
.block();
System.out.println("Input value has been filtered out. No 'next' value " +
"received, just 'completion' signal:" + result.getType());
Its output:
NULL VALUE NOT ACCEPTED !
Error propagated: Bouh !
Input value has been filtered out. No 'next' value received, just 'completion' signal:onNext
Then, let's look at a program that intercept empty pipelines and errors, and handle them gracefully:
// Errors can be intercepted and replaced by a value:
var result = Mono.error(new IllegalStateException("No !"))
.onErrorResume(err -> Mono.just("Override error: Hello again !"))
.block();
System.out.println(result);
// Empty pipelines can also be replaced by another one that produce a value:
result = Mono.just("Hello !")
.filter(it -> !it.endsWith("!"))
.switchIfEmpty(Mono.just("Override empty: Hello again !"))
.block();
System.out.println(result);
It produces:
Override error: Hello again !
Override empty: Hello again !
With all this tools, we can solve the problem you describe with your query.
Let's mimic it:
public static Flux<String> processRequest(String queryParam) {
if (queryParam == null || queryParam.isEmpty()) return Flux.error(new IllegalArgumentException("Bad request"));
return Mono.just(queryParam)
.flatMapMany(param -> Flux.fromArray(param.split("_")))
.switchIfEmpty(Mono.error(new IllegalStateException("No data")));
}
public static void main(String[] args) {
String[] inputs = { null, "hello_world", "___" };
for (var input : inputs) {
try {
String result = processRequest(input)
.collect(Collectors.joining(", ", "[", "]"))
.block();
System.out.println("COMPLETED: " + result);
} catch (Exception e) {
System.out.println("ERROR: " + e.getMessage());
}
}
}
It prints:
ERROR: Bad request
COMPLETED: [hello, world]
ERROR: No data