简体   繁体   中英

Return relevant ServerResponse in case of Flux.error

I have a WebFlux functional REST endpoint and I am having trouble returning custom http errors as a result of exceptions thrown in my code, for example a BadRequest on an invalid path variable.

Consider my handler:

public Mono<ServerResponse> getStarships(ServerRequest request) {
    String starshipType = request.pathVariable("type");
    return ServerResponse
            .ok()
            .contentType(APPLICATION_JSON)
            .body(starshipService.getFromSpacedock(starshipType), Starship.class)
            .onErrorResume(InvalidStarshipTypeException.class, 
                           e -> ServerResponse
                                  .badRequest()
                                  .bodyValue(e.getMessage()));
}

When starshipService.getFromSpacedock(starshipType) returns Flux.just(new Starship()) , all is as expected.

When it returns Flux.error(new InvalidStarshipTypeException("invalid starship type")) , I expect the onErrorResume to kick in and return my custom BadRequest ServerResponse with my message.

Instead, my endpoint responds with 500 (with my custom exception wrapped in it). The onErrorResume is ignored.

How do I solve this?

What I have tried:

  • wrap the exception in a ResponseStatusException : I get my 400 but not through the custom ServerResponse route. The problem with this approach is that I would have to configure Spring to show messages when handling exceptions this way, which I do not want.
  • use flatMap on the Flux, but this results in a Flux<ServerResponse> instead of a Mono<ServerResponse> :
return starshipService.getFromSpacedock(starshipType) // remember, this is a Flux<Starship>
    .flatMap(ships -> ServerResponse.ok()
                          .contentType(MediaType.APPLICATION_JSON)
                          .body(ships, StarShip.class))
    .onErrorResume(e -> ServerResponse.badRequest().bodyValue(e.getMessage()));

I had a similar issue. To solve this issue, you need to first convert your cold flux into a hot flux. Then on the hot flux call .next() , to return a Mono<Starship> . On this mono, call .flatMap().switchIfEmpty().onErrorResume() . In the flatMap() concat the returned startship object with the hot flux.

Here's the code snippet modified to achieve what you want:

public Mono<ServerResponse> getStarships(ServerRequest request) 
{
String starshipType = request.pathVariable("type");

Flux<Starship> coldStarshipFlux = starshipService.getFromSpacedock(starshipType);

//The following step is a very important step. It converts your cold flux into a hot flux.
Flux<Startship> hotStarshipFlux = coldStarshipFlux
                                           .publish()
                                           .refCount(1, Duration.ofSeconds(2));

return hotStarshipFlux.next()
                    .flatMap( starShipObj ->
                       {
                            Flux<Starship> flux = Mono.just(starShipObj)
                                                     .concatWith(hotStarshipFlux);
                            
                            return ServerResponse.ok()
                                          .contentType(MediaType.APPLICATION_JSON)
                                          .body(flux, Starship.class);
                        }
                    )
                    .switchIfEmpty(
                        ServerResponse.notFound().build()
                    )
                    .onErrorResume( t ->
                        {
                            if(t instanceof InvalidStarshipTypeException)
                            {
                                return ServerResponse.badRequest()
                                                    .contentType(MediaType.TEXT_PLAIN)
                                                    .bodyValue(t.getMessage());
                            }
                            else
                            {
                                return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                                    .contentType(MediaType.TEXT_PLAIN)
                                                    .bodyValue(t.getMessage());
                            }
                        });
}

The code .publish().refCount(1, Duration.ofSeconds(2)); is what makes your cold flux into a hot flux. It is important that you do this.

When using a hot flux, each new subscription shares the elements emitted in the flux stream, which are emitted after the subscription began. So, the initial call to .next() causes the hot flux to emit the first element. Now, when the .concatWith() method is called again on the same hot flux, the hot flux will not re-emit the first element again, but will continue emitting the subsequent elements in its stream. So all remaining elements in the stream, starting with the second will get concatenated.

If you did not convert your flux into a hot flux, but ran the above code on your cold flux, then the calls to .next() and .concatWith() would each cause your cold flux to re-generate the same data anew twice, once for .next() , and once for .concatWith() . Thus, you'd have a duplicate first element.

Now, you may ask, why bother with all this hot and cold flux? Why not just do something like the following code snippet using just a flux?焊剂来执行类似以下代码片段的操作? After all a cold will will regenerate the data anew so there won't be a need to call the concatWith() method at all.

Flux<Starship> coldStarshipFlux = starshipService.getFromSpacedock(starshipType);

return coldStarldshipFlux.next()
                         .flatMap( starShipObj -> // ignore the startShipObj
                            {
                                return ServerResponse.ok()
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .body(coldStarldshipFlux, Starship.class);
                            }
                        )
                        .switchIfEmpty(
                             ... //same code as above hot flux
                        )
                        .onErrorResume( t ->
                            {
                                ... //same code as as above hot flux
                            });

The problem with the above code snippet is that all subscriptions on a cold flux re-generate data anew, per subscription. So, effectively the call to .next() and .concatWith() would cause the cold flux to re-generate the same data stream anew, and depending on how your startshipService is coded, may result in a second HTTP request being made. Thus, effectively, you'd be making two HTTP requests instead of one.

When using a hot flux, the need to re-generate data anew (thereby potentially making a second HTTP request) is avoided. That's the biggest advantage of converting your cold flux into a hot flux.

For details on hot and cold flux see the following web site. Towards the end, it very clearly explains the difference between a hot and cold flux, and how they behave differently:
https://spring.io/blog/2019/03/06/flight-of-the-flux-1-assembly-vs-subscription

I hope this answer helps you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM