I'm using Spring Reactor with Spring Cloud Stream (GCP Pub/Sub Binder) and running into error handling issues. I'm able to reproduce the issue with a very simple example:
@Bean
public Function<Flux<String>, Mono<Void>> consumer() {
return flux -> flux
.doOnNext(msg -> log.info("New message received: {}", msg))
.map(msg -> {
if (true) {
throw new RuntimeException("exception encountered!");
}
return msg;
})
.doOnError(throwable -> log.error("Failed to consume message", throwable))
.then();
}
The behavior I expect is to see "Failed to consume message" print, however, that's not what appears to happen. When adding a .log()
call to the chain I see onNext
/onComplete
signals, I would expect to see onError
signals.
My actual code looks something like this:
@Bean
public Function<Flux<CustomMessage>, Mono<Void>> consumer(MyService myService) {
return flux -> flux
.doOnNext(msg -> log.info("New message received: {}", msg))
.flatMap(myService::processMessage) // exception happens deep in here
.doOnError(throwable -> log.error("Failed to consume message", throwable))
.then();
}
I noticed that deep in my service class I was attempting to do error handling on my Reactor publishers. However, the onError
signal wouldn't occur when using Spring Cloud Stream. If I simply invoked my service as such myService.processMessage(msg)
in a unit test and mocked the exception, my reactive chain would propagate error signals correctly.
It seems to be an issue when I hook in to Spring Cloud Stream. I'm wondering if Spring Cloud Function/Stream is doing any global error wrapping?
In my non-trivial code I do notice this error message that may have something to do with why I'm not getting error signals?
ERROR --- onfiguration$FunctionToDestinationBinder : Failed to process the following content which will be dropped: ...
To further my confusion, I am able to get the onError
signal in my reactive chain if I switch my Spring Cloud Stream binding to the non-reactive implementation as so:
@Bean
public Consumer<CustomMessage> consumer(MyService myService) {
return customMessage -> Mono.just(customMessage)
.doOnNext(msg -> log.info("New message received: {}", msg))
.flatMap(myService::processMessage) // exception happens deep in here
.doOnError(throwable -> log.error("Failed to consume message", throwable)) // prints successfully this time
.subscribe();
}