I am at a loss of how to create an appropriate flow using webflux - spring-webflux

Problem statement: I have a POST request to a booking-service API that gets a BookingRecord. I map it to extract the values so as to call another fare-service API using WebClient. I receive a Mono<Fare> from that call. I need to check whether the value of getFare() method of BookingRecord type is same as the getFare() of the Fare type returned by the WebClient. If not, I need to raise and exception, and pass it on to the caller. Here caller is another Microservice, ui-service calling the booking-service API (so how should I deal with this, pass the error back or else what is the best thing to do?) or else I will save the new BookingRecord and return the id of that record to the caller. What is the best flow sequence for this? I tried my best without much success and am pasting the code here.
public HandlerFunction<ServerResponse> book = request ->
{
request.bodyToMono(BookingRecord.class)
.map(br ->
{
this.webClient.get()
.uri("/fares/get/{flightNumber}/{flightDate}",
br.getFlightNumber(),
br.getFlightDate())
.retrieve()
.bodyToMono(Fare.class)
.map(f ->
{
if (!f.getFare()
.equals(br.getFare()))
{
throw new RuntimeException("Fare is tampered");
}
else
{
id = bookingRepository.save(br).getId();
}
return id;
})
.subscribe();
return id;
});
return ServerResponse.ok()
.body(BodyInserters.fromObject(id));
};

After much tweaking, this is what I did. Hope its the right thing to do. 1. I raise a 500 Http error from the fare-service itself instead of checking in the booking-service.
public HandlerFunction<ServerResponse> getFare = request ->
{
String flightNumber = request.pathVariable("flightNumber");
String flightDate = request.pathVariable("flightDate");
String fare = request.pathVariable("fare");
Mono<ServerResponse> notFound = ServerResponse.notFound()
.build();
return Mono
.justOrEmpty(faresRepository.getFareByFlightNumberAndFlightDateAndFare(flightNumber,
flightDate,
fare))
.flatMap(f -> ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(fromObject(f)))
.switchIfEmpty(notFound);
};
Handled the exception like so in booking-service using onStatus() method
public HandlerFunction<ServerResponse> book = request ->
{
logger.info("Inside Book function");
return request.bodyToMono(BookingRecord.class)
.flatMap(br ->
{
logger.info("Calling fare-service");
return this.webClient.get()
.uri("/fares/get/{flightNumber}/{flightDate}/{fare}",
br.getFlightNumber(),
br.getFlightDate(),
br.getFare())
.retrieve()
.onStatus(HttpStatus::isError,
x -> Mono
.error(new RuntimeException("Fare has been tampered with!!")))
.bodyToMono(Fare.class);
})
.map(fare ->
{
logger.info("Saving a BookingRecord");
BookingRecord br = new BookingRecord();
br.setFlightNumber(fare.getFlightNumber());
br.setFlightDate(fare.getFlightDate());
br.setFare(fare.getFare());
long id = bookingRepository.save(br)
.getId();
return id;
})
.flatMap(id -> ServerResponse.ok()
.body(BodyInserters.fromObject(id)));
};
This way, I get an exception for fare tampering or get the id for a successful db save.

Related

Java reactor and variable scope

I am trying to get my head around variable propagation using reactor. I have a function as follow where I am trying to pass a variable named requestName from outside the map as follow:
public Mono<ResponseEntity<? extends Object>> myFunction(
final Object request, final String requestName) {
return this.client
.create(request)
.exchangeToMono(
response -> {
final HttpStatus status = response.statusCode();
return response
.bodyToMono(String.class)
.defaultIfEmpty(StringUtils.EMPTY)
.map(
body -> {
if (status.is2xxSuccessful()) {
log.info("{}", requestName);
return ResponseEntity.ok().build();
} else {
return ResponseEntity.badRequest().body(null);
}
});
})
.onErrorResume(ex -> Mono.just(buildErrorFromException(requestName, ex)));
}
or another example would be:
String myvar = "test"
return this.
.login()
.flatMap(
response ->
this.myservice(myvar))
.flatMap(
response2 ->
this.myservice2(myvar))
Is it appropriate ? Or would i need to wrap this function around a Mono.deferContextual and apply a contextView ?
Thanks a lot for your help.

How to return a Flux in async/reactive webclient request with subscribe method

I am using spring hexagonal architecture (port and adapter) as my application need to read the stream of data from the source topic, process/transforms the data, and send it to destination topic.
My application need to do the following actions.
Read the data (which will have the call back url)
Make an http call with the url in the incoming data (using webclient)
Get the a actual data and it needs to be transformed into another format.
Send the transformed data to the outgoing topic.
Here is my code,
public Flux<TargeData> getData(Flux<Message<EventInput>> message)
{
return message
.flatMap(it -> {
Event event = objectMapper.convertValue(it.getPayload(), Event.class);
String eventType = event.getHeader().getEventType();
String callBackURL = "";
if (DISTRIBUTOR.equals(eventType)) {
callBackURL = event.getHeader().getCallbackEnpoint();
WebClient client = WebClient.create();
Flux<NodeInput> nodeInputFlux = client.get()
.uri(callBackURL)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptTypes = new ArrayList<>();
acceptTypes.add(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(acceptTypes);
})
.exchangeToFlux(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(NodeInput.class);
}
return Flux.empty();
});
nodeInputFlux.subscribe( nodeInput -> {
SourceData source = objectMapper.convertValue(nodeInput, SourceData.class);
// return Flux.fromIterable(this.TransformImpl.transform(source));
});
}
return Flux.empty();
});
}
The commented line in the above code is giving the compilation as subscribe method does not allow return types.
I need a solution "without using block" here.
Please help me here, Thanks in advance.
I think i understood the logic. What do you may want is this:
public Flux<TargeData> getData(Flux<Message<EventInput>> message) {
return message
.flatMap(it -> {
// 1. marshall and unmarshall operations are CPU expensive and could harm event loop
return Mono.fromCallable(() -> objectMapper.convertValue(it.getPayload(), Event.class))
.subscribeOn(Schedulers.parallel());
})
.filter(event -> {
// 2. Moving the if-statement yours to a filter - same behavior
String eventType = event.getHeader().getEventType();
return DISTRIBUTOR.equals(eventType);
})
// Here is the trick 1 - your request below return Flux of SourceData the we will flatten
// into a single Flux<SourceData> instead of Flux<List<SourceData>> with flatMapMany
.flatMap(event -> {
// This WebClient should not be created here. Should be a singleton injected on your class
WebClient client = WebClient.create();
return client.get()
.uri(event.getHeader().getCallbackEnpoint())
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(SourceData.class);
}
return Flux.empty();
});
})
// Here is the trick 2 - supposing that transform return a Iterable of TargetData, then you should do this and will have Flux<TargetData>
// and flatten instead of Flux<List<TargetData>>
.flatMapIterable(source -> this.TransformImpl.transform(source));
}

How to return bad request in spring webflux when there is an error?

I have this server endpoint using spring-webflux and I would like to return ServerResponse.badRequest() when the serverRequest receives a wrong parameter. The request curl -s "http://localhost:8080/4?a=5&b=3"; echo for instance, contains the right parameters. But the request curl -s "http://localhost:8080/one?a=5&b=3"; echo contains a string instead of an Integer. Then the conversion new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()) will throw an error.
I was doing .onErrorReturn(new BidRequest(0, null)) but now I want to implement some operation that return ServerResponse.badRequest(). So I added in the end .onErrorResume(error -> ServerResponse.badRequest().build()) in the end, but It is not working. I also added on the place of the code .onErrorReturn() and it does not compile.
public Mono<ServerResponse> bidRequest(ServerRequest serverRequest) {
var adId = serverRequest.pathVariable("id");
var attributes = serverRequest.queryParams();
log.info("received bid request with adID: {} attributes: {}", adId, attributes);
return Mono.just(Tuples.of(adId, attributes))
.map(tuple2 -> new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()))
// I WANT TO REPLACE IT FOR A BAD REQUEST
// .onErrorReturn(new BidRequest(0, null))
.flatMap(bidRequest -> {
return Flux.fromStream(bidderService.bidResponseStream(bidRequest))
.flatMap(this::gatherResponses)
.reduce((bidResp1, bidResp2) -> {
if (bidResp1.getBid() > bidResp2.getBid()) return bidResp1;
else return bidResp2;
});
})
.map(bid -> {
var price = bid.getContent().replace("$price$", bid.getBid().toString());
bid.setContent(price);
return bid;
})
.flatMap(winner -> {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(winner.getContent()));
})
.switchIfEmpty(ServerResponse.notFound().build())
// THIS DOES NOT RETURN ANY BAD REQUEST
.onErrorResume(error -> ServerResponse.badRequest().build());
}
I solved based on this answer using flatmap and returning a Mono.just() or a Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));.
return Mono
.just(Tuples.of(adId, attributes))
.flatMap(tuple2 -> {
if (validate(tuple2)) {
log.info("request parameters valid: {}", tuple2);
return Mono.just(new BidRequest(Integer.parseInt(tuple2.getT1()), tuple2.getT2().toSingleValueMap()));
} else {
log.error("request parameters invalid: {}", tuple2);
return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
})
.flatMap(....
private boolean validate(Tuple2<String, MultiValueMap<String, String>> tuple2) {
return GenericValidator.isInteger(tuple2.getT1());
}

webClient response using bodyToFlux method merging all the received response instead separating it out

I am using ParallelFlux for running many task.
but when i am receiving webClient response using bodyToFlux method its merging all the output response instead of getting one by one.
i want the output should be one by one not single string, is there any other method instead of bodyToFLux need to use.
request method:
Flux<String> responsePost = webClient.build()
//removed get,url and retrieve here
.bodyToFlux(String.class);
responsePost.subscribe(s -> {
//display response
});
response method:
public ParallelFlux<String> convertListToMap() {
//created list of string str
return Flux.fromIterable(str)
.parallel(3)
.runOn(Schedulers.parallel())
.map( s -> {
//some logic here
});
}
output:
parallel fulx reponse: springwebfluxparellelExample
Here instead of returning ParallelFlux create an
ResponseBody class with variable you want to return, assign your response to
variable inside ResponseBody and return it to caller of API function.
public ParallelFlux<ResponseBody> convertListToMap() {
//created list of string str
return Flux.fromIterable(str)
.parallel(3)
.runOn(Schedulers.parallel())
.map( s -> {
//some logic here
});
}
Flux<String> responsePost = webClient.build()
//removed get,url and retrieve here
.bodyToFlux(ResponseBody.class);

Handling Global Scenarios in Spring WebFlux

I have a Rest Web Client todo an API call and I handle the exceptions as given below.
I want to handle 404, 401 and 400 errors in a global way rather than handling at the individual client level. How can we achieve the same.
public Mono<ProductResponse> getProductInformation(String productId) {
return webClient.get()
.uri("/v1/products/"+productId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus( httpStatus -> HttpStatus.NOT_FOUND.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return notFound.flatMap( msg -> {
log.info(" Error Message {}" , msg.getErrorMsg());
return Mono.error(new NotFoundException(msg.getErrorMsg()));
});
}).onStatus( httpStatus -> HttpStatus.UNAUTHORIZED.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return Mono.error(new NotAuthorisedException("Unauthorised"));
}).bodyToMono(ProductResponse.class);
}
Two approaches:
Exceptions with webclients are all wrapped in WebClientResponseException class. You can handle that using Spring's ExceptionHandler annotation like this.
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity handleWebClientException(WebClientResponseException ex){
return ResponseEntity.badRequest().body(ex.getResponseBodyAsString());
}
Note - Here you can write complex conditional logic based on the response status, by using methods like getStatusCode(), getRawStatusCode(), getStatusText(), getHeaders() and getResponseBodyAsString(). Also you can get reference of the request that was sent using the method getRequest.
Using ExchangeFilterFunction while constructing the webclient bean.
#Bean
public WebClient buildWebClient() {
Function<ClientResponse, Mono<ClientResponse>> webclientResponseProcessor =
clientResponse -> {
HttpStatus responseStatus = clientResponse.statusCode();
if (responseStatus.is4xxClientError()) {
System.out.println("4xx error");
return Mono.error(new MyCustomClientException());
} else if (responseStatus.is5xxServerError()) {
System.out.println("5xx error");
return Mono.error(new MyCustomClientException());
}
return Mono.just(clientResponse);
};
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(webclientResponseProcessor)).build();
}
Then you can either handle the MyCustomClientException using #ExceptionHandler or leave it as it is.