7

I am using the functional endpoints of WebFlux. I translate exceptions sent by the service layer to an HTTP error code using onErrorResume:

public Mono<String> serviceReturningMonoError() {
    return Mono.error(new RuntimeException("error"));  
}

public Mono<ServerResponse> handler(ServerRequest request) {
    return serviceReturningMonoError().flatMap(e -> ok().syncBody(e))
                            .onErrorResume( e -> badRequest(e.getMessage()));
}

It works well as soon as the service returns a Mono. In case of a service returning a Flux, what should I do?

public Flux<String> serviceReturningFluxError() {
    return Flux.error(new RuntimeException("error"));  
}

public Mono<ServerResponse> handler(ServerRequest request) {
    ???
}

Edit

I tried the approach below, but unfortunately it doesn't work. The Flux.error is not handled by the onErrorResume and propagated to the framework. When the exception is unboxed during the serialization of the http response, Spring Boot Exception management catch it and convert it into a 500.

public Mono<ServerResponse> myHandler(ServerRequest request) {
      return ok().contentType(APPLICATION_JSON).body( serviceReturningFluxError(), String.class)
             .onErrorResume( exception -> badRequest().build());
}

I am actually surprised of the behaviour, is that a bug?

Nicolas Barbé
  • 604
  • 7
  • 12

3 Answers3

4

I found another way to solve this problem catching the exception within the body method and mapping it to ResponseStatusException

public Mono<ServerResponse> myHandler(ServerRequest request) {
    return ok().contentType(MediaType.APPLICATION_JSON)
            .body( serviceReturningFluxError()
                    .onErrorMap(RuntimeException.class, e -> new ResponseStatusException( BAD_REQUEST, e.getMessage())), String.class);
}

With this approach Spring properly handles the response and returns the expected HTTP error code.

Nicolas Barbé
  • 604
  • 7
  • 12
2

Your first sample is using Mono (i.e. at most one value), so it plays well with Mono<ServerResponse> - the value will be asynchronously resolved in memory and depending on the result we will return a different response or handle business exceptions manually.

In case of a Flux (i.e. 0..N values), an error can happen at any given time.

In this case you could use the collectList operator to turn your Flux<String> into a Mono<List<String>>, with a big warning: all elements will be buffered in memory. If the stream of data is important of if your controller/client relies on streaming data, this is not the best choice here.

I'm afraid I don't have a better solution for this issue and here's why: since an error can happen at any time during the Flux, there's no guarantee we can change the HTTP status and response: things might have been flushed already on the network. This is already the case when using Spring MVC and returning an InputStream or a Resource.

The Spring Boot error handling feature tries to write an error page and change the HTTP status (see ErrorWebExceptionHandler and implementing classes), but if the response is already committed, it will log error information and let you know that the HTTP status was probably wrong.

Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • Thanks, great explanations! It's hard to completely get rid of old synchronous thinking :) To solve my problem I changed my service API with additional methods to perform those checks before calling the asynchronous call. – Nicolas Barbé Feb 12 '18 at 11:50
  • 1
    I have added some examples in t[his post](https://stackoverflow.com/questions/45752104/how-to-return-404-with-spring-webflux/45769293#45769293) – Hantsy May 17 '18 at 10:15
0

Though this is an old question, I'd like to answer it for anyone who may stumble upon this Stack Overflow post.

There is another way to address this particular issue (discussed below), without the need to cache / buffer all the elements in memory as detailed in one of the other answers. However, the approach shown below does have a limitation. First, I'll discuss the approach, then the limitation.

The approach
You need to first convert your cold flux into a hot flux. Then on the hot flux call .next(), to return a Mono<Your Object> On this mono, call .flatMap().switchIfEmpty().onErrorResume(). In the flatMap() concatenate the returned Your Object with the hot flux stream.

Here's the original code snippet posted in the question, modified to achieve what is needed:

public Flux<String> serviceReturningFluxError()
{
    return Flux.error(new RuntimeException("error"));  
}


public Mono<ServerResponse> handler(ServerRequest request)
{
    Flux<String> coldStrFlux = serviceReturningFluxError();

    // The following step is a very important step. It converts the cold flux 
    // into a hot flux.
    Flux<String> hotStrFlux = coldStrFlux.publish().refCount(1, Duration.ofSeconds(2));

    return hotStrFlux.next()
                     .flatMap( firstStr ->
                       {
                            Flux<String> reCombinedFlux = Mono.just(firstStr)
                                                .concatWith(hotStrFlux);
                            
                            return ServerResponse.ok()
                                          .contentType(MediaType.APPLICATION_JSON)
                                          .body(reCombinedFlux, String.class);
                        }
                    )
                    .switchIfEmpty(
                        ServerResponse.notFound().build()
                    )
                    .onErrorResume( throwable -> ServerResponse.badRequest().build() );
}

The reason for converting from cold to hot Flux is that by doing so, a second redundant HTTP request is not made.

For a more detailed answer please refer to the following Stack Over post, where I've commented upon this in greater detail: Return relevant ServerResponse in case of Flux.error

Limitation
While the above approach will work for exceptions / Flux.error() streams returned from the service, it will not work for any exceptions that may arise while emitting the individual elements from the flux after the first element is successfully emitted.

The assumption in the above code is simple. If the service throws an exception, then the very first element returned from the service will be a Flux.error() element. This approach does not account for the fact that exceptions may be thrown in the returned Flux stream after the first element, say possibly due to some network connection issue that occurs after the first few elements are already emitted by the Flux stream.

Mody
  • 86
  • 3