Return 404 when a Flux is empty - spring-webflux

I am trying to return a 404 when a Flux is empty, similar to here:WebFlux functional: How to detect an empty Flux and return 404?
My main concern is that, when you check if the flux has elements it emmits that value and you loose it. And when I try to use switch if empty on the Server Response it is never called (I secretly think it is because the Mono is not empty, only the body is empty).
Some code of what I am doing (I do have a filter on my Router class checking for DataNotFoundException to return a notFound):
Flux<Location> response = this.locationService.searchLocations(searchFields, pageToken);
return ok()
.contentType(APPLICATION_STREAM_JSON)
.body(response, Location.class)
.switchIfEmpty(Mono.error(new DataNotFoundException("The data you seek is not here.")));
^This never calls switchIfEmpty
Flux<Location> response = this.locationService.searchLocations(searchFields, pageToken);
return response.hasElements().flatMap(l ->{
if(l){
return ok()
.contentType(APPLICATION_STREAM_JSON)
.body(response, Location.class);
}
else{
return Mono.error(new DataNotFoundException("The data you seek is not here."));
}
});
^This looses the emitted element on hasElements.
Is there a way to either recover the emitted element in hasElements or to make the switchIfEmpty only check the contents of the body?

You could apply switchIfEmpty operator to your Flux<Location> response.
Flux<Location> response = this.locationService
.searchLocations(searchFields, pageToken)
.switchIfEmpty(Mono.error(new DataNotFoundException("The data you seek is not here.")));

while the posted answers are indeed correct, there is a convenience exception class if you just want to return a status code (plus a reason) and do not want to fiddle with any custom filters or defining your own error response exceptions.
The other benefit is that you do not have to wrap your responses inside of any ResponseEntity Objects, while useful for some cases (for example, created with a location URI), is an overkill for simple status responses.
see also https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/ResponseStatusException.html
return this.locationService.searchLocations(searchFields, pageToken)
.buffer()
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "these are not the droids you are lookig for")));

What Alexander wrote is correct. You call switchIfEmpty on the Object that is never empty ServerResponse.ok() by definition is not a empty Publisher. I like to handle this cases in revers so invoke the service and then chain all the methods that create the response.
this.locationService.searchLocations(searchFields, pageToken)
.buffer()
.map(t -> ResponseEntity.ok(t))
.defaultIfEmpty(ResponseEntity.notFound().build());
UPDATE (not sure if it works, but give it a try):
public Mono<ServerResponse> myRestMethod(ServerRequest serverRequest) {
return serverRequest.bodyToMono(RequestDTO.class)
.map((request) -> searchLocations(request.searchFields, request.pageToken))
.flatMap( t -> ServerResponse
.ok()
.body(t, ResponseDTO.class)
)
.switchIfEmpty(ServerResponse.notFound().build())
;
}

Related

Spring Reactor and Vavr Either

We are trying to combine reactive programming with Either from Vavr.
The basic idea is to have a controller that returns a Mono but the input has to go through a bunch of filters which should return an Either with the reason for the filter rejecting it on left and a possibly augmented input on right.
Next are a few API calls which again should return an Either with the error response on left and the success response on right so we can act accordingly.
We are currently struggling to get this together but there is too much unwrapping in each map.
The ugly fallback solution is to throw custom errors instead so we could catch them in the onErrorResume.
I currently have something like this just for the filter part where are the filters are looped over in the doFilter which returns one Either for all of them, but then it gets tricky with the API calls as this currently only return the happy path and catches exceptions.
public Mono<OutputType> runPipeline(InputType input) {
return Mono.just(input)
.flatMap(
o -> doFilter(o)
.map(this::getAPIResponse)
.getOrElse(getFallback(input)))
.onErrorResume(err -> {
return getFallback(input);
});
}
private Either<String, InputType> doFilter(InputType input) {
for (Filter next : filters) {
var result = next.filter(input);
if (!result.equals(SUCCESS)) {
return Either.left("A filter denied processing");
}
}
return Either.right(input);
}
Maybe we are just looking at it wrong, so any alternative solution would be welcome.

Spring Webflux returning null back to controller

Every time I think I understand Webflux and project reactor, I find out I have no idea.
So I making some API calls... I want to call 1 first ... Get information back use that information, to make subsequent calls.
so I do this like so
public Mono<ResponseObject> createAggregatedRecords(RecordToPersist recordToPersist){
return randomApiClient.createRecord(recordToPersist)
.flatMap(result -> {
return Mono.zip(
webClientInstance.createOtherRecord1(result.getChildRecord1()),
webClientInstance2.createOtherRecord2(result.getChildRecord2()),
webClientInstance3.createOtherRecord3(result.getChildRecord3()))
.map(tupple -> {
ResponseObject respObj = new ResponseObject();
respObj.setChildResult1(tupple.getT1());
respObj.setChildResult2(tupple.getT2());
respObj.setChildResult3(tupple.getT3());
return respObj;
}
}).doOnSuccess(res -> log.info("This throws an error: {}", res.getChildResult1.getFirstField()))
}
Now, for some reason, I am returning a null object with this very code to my Controller and I am not printing out the object in Json.
I suspect it is because I am nesting the Mono.zip inside the flatmap, and am not returning the results back correctly. I am making all of those API calls though as my End-to-End integration tests are succeeding.
Now I thought that I would return that response object from the .map function from the Mono.zip chain and then return that to the flatMap call in the chain. If I put observers on the chain like a doOnSuccess and print out response object fields I get a null pointer ... Not sure what I am missing
Is this a good pattern to achieve that goal? Or should I try a different path?
Why can I not get the response Object to return?

How to, on a find by id API, send body with HTTP status 200 or empty body with HTTP status 204 using spring webFlux?

Even with this code I get an emtpy response with 200 when I look for an non existing person's id. How can I set different statuses based on personManager.findById result? I come from an imperative background, and maybe this is silly, but I didn't find any consensus on how to do it, even on official docs
fun get(request: ServerRequest): Mono<ServerResponse> =
ServerResponse
.ok().contentType(MediaType.APPLICATION_JSON)
.body(
BodyInserters.fromPublisher(
personManager.findById(request.pathVariable("id").toInt()),
Person::class.java
)
).switchIfEmpty(ServerResponse.noContent().build())
The problem was that .body always return a Mono filled with a ServerResponse, so it would never switch to empty. I really don't know if this is syntactic right, but I managed to do what I wanted with:
personManager.findById(request.pathVariable("id").toInt()).flatMap {
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(it)
}.switchIfEmpty(ServerResponse.noContent().build())
and the same behavior to Flux(in this case, findAll):
with(personManager.findAll()) {
this.hasElements().flatMap {
if (it) ServerResponse.ok().bodyValue(this)
else ServerResponse.noContent().build()
}
}

Flux collectList() on list of WebClient exchanges always empty

I'm trying to execute a list requests using WebClient, then filter them finding the first one that succeed (if any) and return that. Or fall back to a default response if non succeeded.
The problem I'm facing is that when I call .collectList() on a Flux<ServerResponse>, the list is always empty. I would have expected the list to contain N number of ServerResponse based on the number of requests I issued earlier.
public Mono<ServerResponse> retry(ServerRequest request) {
return Flux.fromIterable(request.headers().header(SEQUENCE_HEADER_NAME))
.map(URI::create)
// Build a "list" of responses
.flatMap(uri -> webClientBuilder.baseUrl(uri.toString()).build()
.method(Objects.requireNonNull(request.method()))
.headers(headers -> request.headers().asHttpHeaders().forEach((key, values) -> {
if (!SEQUENCE_HEADER_NAME.equals(key)) {
headers.addAll(key, values);
}
}))
.body(BodyInserters.fromDataBuffers(request.body(BodyExtractors.toDataBuffers())))
.exchange()
.flatMap(clientResponse -> ServerResponse.status(clientResponse.statusCode())
.headers(headers -> headers.addAll(clientResponse.headers().asHttpHeaders()))
.body(BodyInserters.fromDataBuffers(clientResponse.body(BodyExtractors.toDataBuffers()))))
)
// "Wait" for all of them to complete so we can filter
.collectList()
.flatMap(clientResponses -> {
List<ServerResponse> filteredResponses = clientResponses.stream()
.filter(response -> response.statusCode().is2xxSuccessful())
.collect(Collectors.toList());
if (filteredResponses.isEmpty()) {
log.error("No request succeeded; defaulting to {}", HttpStatus.BAD_REQUEST.toString());
return ServerResponse.badRequest().build();
}
if (filteredResponses.size() > 1) {
log.error("Multiple requests succeeded; defaulting to {}", HttpStatus.BAD_REQUEST.toString());
return ServerResponse.badRequest().build();
}
return Mono.just(filteredResponses.get(0));
});
}
Any ideas why .collectList() always returns an empty list?
Well, it seems to me you have a confused requirement in that you want the First Mono that responds but you are trying to put that functionality into a Flux which is meant to process all items in the flow efficiently. Mono in Webflux is meant to create a flow that will perform a series of transformations on the item in the flow efficiently. Nothing in your requirement of testing a bunch of URIs for the first one that succeeds is what WebFlux is good for so I have to question why try to force that into the framework.
You might argue that a Flux is giving you better asynchronous processing but I don't think that's the case when it is a bunch of WebClient calls. WebClient is still HTTP under the hood and so each item in the flow stops and starts around WebClient. If you want to do HTTP asynchronously you should use a ThreadPool and Callable.

RxJava2 merge cached data and subject

I have an disk storage that returns an Object? from disk (Could be already saved or not) and an BehaviourSubject (This data comes from other class call, check code below):
Code is:
private val subject: Subject<Optional<Element>> = BehaviorSubject.create()
fun getElements(): Observable<List<Element>> =
Observable.concat(Observable.just(storage.getElement()), subject)
.filter({ it.isPresent })
.take(1)
.flatMapSingle {
Observable.just(it.get())
.flatMapIterable { it.categories }
.toList()
}
fun updateSubject(response: Response) {
storage.save(response.element) //Save element in storage
subject.onNext(response.element.toOptional())
}
My problem is, in other class I do
getElements().subscribe(onElements(), onError());
First time, when storage has null it does nothing, even I've got a breakpoint in subject.onNext(response.element.toOptional()), hoping that onNext will trigger a stream for getElements, but nothing happens.
Second time, when I've already saved in storage the received element (So, storage.getElement() returns something) it works fine.
My functional description is:
Get element from both cache and subject, take first that arrives, and return it (First time it will be who the comes subject one), next time, i'm hoping that first one will be the storage one.
I am assuming that storage is some sort of persistence object so it so storage.getElement() might return a valid object at the time you create the subject?
If that is the case, then I think you should check to see if you have a stored object before you create the subject, if so use BehaviorSubject.createDefalut(storedObject) if it does exist or BehaviorSubject.create() if not.
Then inside your getElements() function I think you can just use subject.filter() ....
Your null optional elements are being filtered out by your .filter { it.isPresent } call so nothing gets emitted downstream.