Is it possible to pass Flux to the body of webClient POST? - spring-webflux

I have an endpoint like this :
#PostMapping("/products")
Flux<Product> getProducts(#RequestBody Flux<String> ids) {
return Flux...
}
On my client side, I want to consume this endpoint but not sure how to pass a Flux of String in the body (I don't want to make it a list)
Flux<Product> getProducts(Flux<String> ids) {
return webClient.post().uri("/products")
.body(/* .. how should I do here? ..*/)
.retrieve()
.bodyToFlux(Product.class);
}

You can, in fact, pass in a Flux into the .body() method on the WebClient
Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
Example taken from Spring Reference Docs
The variation of the body() method you'd want to use is:
<T,P extends org.reactivestreams.Publisher<T>> WebClient.RequestHeadersSpec<?> body(P publisher,
Class<T> elementClass)
Relevant JavaDoc for this method

Related

Aggregating requests in WebFlux

Hi I am trying with Spring WebFlux and reactor.
What I want to achieve is to aggregate some requests into one call to a third party API. And then return the results. My code looks like this:
#RestController
#RequiredArgsConstructor
#RequestMapping(value = "/aggregate", produces = MediaType.APPLICATION_JSON_VALUE)
public class AggregationController {
private final WebClient externalApiClient;
//sink that stores the query params
private Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
#GetMapping
public Mono<Mono<Map<String, BigDecimal>>> aggregate(#RequestParam(value = "pricing", required = false) List<String> countryCodes) {
//store the query params in the sink
countryCodes
.forEach(sink::tryEmitNext);
return Mono.from(
sink.asFlux()
.distinct()
.bufferTimeout(5, Duration.ofSeconds(5))
)
.map(countries -> String.join(",", countries))
.map(concatenatedCountries -> invokeExternalPricingApi(concatenatedCountries))
.doOnSuccess(s -> sink.emitComplete((signalType, emitResult) -> emitResult.isSuccess()));
}
private Mono<Map<String, BigDecimal>> invokeExternalPricingApi(String countryCodes) {
return externalApiClient.get().uri(uriBuilder -> uriBuilder
.path("/pricing")
.queryParam("q", countryCodes)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {
});
}
}
I have the Sinks.Many<String> sink to accumulate the query params from the callers of the API. When there are 5 items or 5 seconds expired, I want to call the third party API. The issue is that when I do two requests like:
GET localhost:8081/aggregate?pricing=CH,EU
GET localhost:8081/aggregate?pricing=AU,NZ,PT
On one request I get the response and on the other I get null. Also, after the first interaction the system stops working as if the sink was broken.
What am I missing here?

WebClient synchronous call does not return from within a filter stack trace

In a Spring Gateway API I have a filter which calls a class to make a call to another API using WebClient. If I make the same call from say a controller the call returns. However when this webclient call is made from within the Filter stack it never returns. I am trying to make this call synchronously. I cannot use the block() method because Reactive classes error.
Here is the method in question:
public void doPost() {
ApiResponse<Void> response = webClientBuilder.build().post()
.uri("http://localhost:8080")
.retrieve()
.bodyToMono(new ParameterizedTypeReference<ApiResponse<Void>>() {})
.block();
}
I am very new to WebClient and need someone to tell me how I can synchronously make this call. I have tried another variation which is toFuture().get() instead of the last line but this also does not return.
It get the below error:
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-4
My mistake it is an authentication filter that this is being run from:
public class AuthServiceAuthenticationManager implements ReactiveAuthenticationManager {
private final MyClient myClient;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
//Below line does not return using my webclient
myClient.post();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), new ArrayList<GrantedAuthority>());
return Mono.just(token);
}
}
As I mentioned in comment, the reason is simple - you and blocking doPost is called from the reactive flow. WebClient is a non-blocking client and as you are using it from the ReactiveAuthenticationManager you could keep the whole flow reactive.
Solution:
Remove block() from the doPost and return Mono.
public Mono<ApiResponse<Void>> doPost() {
return webClientBuilder.build().post()
.uri("http://localhost:8080")
.retrieve()
.bodyToMono(new ParameterizedTypeReference<ApiResponse<Void>>() {})
}
Construct reactive flow in AuthServiceAuthenticationManager.
Logic of authenticate is not really clear but based on your example it could look like
public Mono<Authentication> authenticate(Authentication authentication) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), new ArrayList<>());
return doPost()
.thenReturn(token);
}

How to get ClientResponse body as DataBuffer in WebClient.exchangeToMono Spring 5.3?

Before the deprecation of WebClient.exchange method I used to get ClientResponse body as Flux<DataBuffer> and manipulated it.
In Spring 5.3 the exchange() method is deprecated and I would like to change the implementation as recommended:
#deprecated since 5.3 due to the possibility to leak memory and/or
connections; please,
use {#link #exchangeToMono(Function)}, {#link #exchangeToFlux(Function)};
consider also using {#link #retrieve()} ...
Tried to do the same call in the lambda passed to exchangeToMono, but clientResponse.bodyToFlux(DataBuffer::class.java) always return an empty flux; other experiments (i.e. getting body as mono string) also could not help to get the body.
What is the standard way to get the ClientResponse body in Spring 5.3 ?
I am looking for a low-level body representation: something like "data buffer", "byte array" or "input stream"; to avoid any kind of parsing/deserialisation.
Before Spring 5.3:
webClient
.method(GET)
.uri("http://somewhere.com")
.exchange()
.flatMap { clientResponse ->
val bodyRaw: Flux<DataBuffer> = clientResponse.bodyToFlux(DataBuffer::class.java)
// ^ body as expected
// other operations
}
After Spring 5.3
webClient
.method(GET)
.uri("http://somewhere.com")
.exchangeToMono { clientResponse ->
val bodyRaw: Flux<DataBuffer> = clientResponse.bodyToFlux(DataBuffer::class.java)
// ^ always empty flux
// other operations
}
The new exchangeToMono and exchangeToFlux methods expect the body to be decoded inside the callback. Check this GitHub issue for details.
Looking at your example, probably you could use retrieve, that is safer alternative, with bodyToFlux
webClient
.method(GET)
.uri("http://somewhere.com")
.retrieve()
.bodyToFlux(DataBuffer.class)
or toEntityFlux if you need access to response details like headers and status
webClient
.method(GET)
.uri("http://somewhere.com")
.retrieve()
.toEntityFlux(DataBuffer.class)
Handling errors
Option 1. Use onErrorResume and handle WebClientResponseException
webClient
.method(GET)
.uri("http://somewhere.com")
.retrieve()
.bodyToFlux(DataBuffer.class)
.onErrorResume(WebClientResponseException.class, ex -> {
if (ex.getStatusCode().equals(HttpStatus.NOT_FOUND)) {
// ignore 404 and return empty
return Mono.empty();
}
return Mono.error(ex);
});
Option 2. Use onStatus convenience method to get access to response.
webClient
.method(GET)
.uri("http://somewhere.com")
.retrieve()
.onStatus(status -> status.equals(HttpStatus.NOT_FOUND), res -> {
// ignore 404 and return empty
return Mono.empty();
})
.bodyToFlux(DataBuffer.class)
Both methods could be used to deserialize error response Getting the response body in error case with Spring WebClient

Spring webflux : consume mono or flux from request

I have a resource API that handles an object (Product for example).
I use PUT to update this object in the database.
And I want to return just en empty Mono to the user.
There is my code :
public Mono<ServerResponse> updateProduct(ServerRequest request){
Mono<Product> productReceived = request.bodyToMono(Product.class);
Mono<Product> result = productReceived.flatMap(item -> {
doSomeThing(item);
System.out.println("Called or not called!!");
return Mono.just(productService.product);
}).subscribe();
return ok()
.contentType(APPLICATION_JSON)
.body(Mono.empty(), Product.class);
}
The problem is my method doSomeThing() and the println are not called.
NB: I use subscribe but doesn't work.
Thanks.
I had a similar issue when I was new to Webflux. In short, you can't call subscribe on the request body and asynchronously return a response because the subscription might not have enough time to read the body. You can see a full explanation of a similar issue here.
To make your code work, you should couple the response with your logic stream. It should be something like the following:
public Mono<ServerResponse> updateProduct(ServerRequest request){
return request
.bodyToMono(Product.class)
.flatMap(item -> {
doSomeThing(item);
System.out.println("Called or not called!!");
return Mono.just(productService.product);
})
.then(ServerResponse.ok().build());
}

Adding custom Response header to Spring WebFlux contoller endpoint

Is there a way to add a response header to spring webflux controller endpoint? for example to the following method I have to add a custom header say 'x-my-header'
#GetMapping(value = "/search/{text}")
#ResponseStatus(value = HttpStatus.OK)
public Flux<SearchResult> search(#PathVariable(
value = "text") String text){
return searchService().find(text);
}
In the functional API, this is really easy; the ServerResponse builder has builders for almost everything you need.
With the annotated controllers; you can return an ResponseEntity<Flux<T>> and set the headers:
#GetMapping(value = "/search/{text}")
public ResponseEntity<Flux<SearchResult>> search(#PathVariable(
value = "text") String text) {
Flux<SearchResult> results = searchService().find(text);
return ResponseEntity.ok()
.header("headername", "headervalue")
.body(results);
}
Note that the updated code doesn't need the #ResponseStatus annotation now.
UPDATE:
Apparently the solution above works; unless you have spring-cloud-starter-netflix-hystrix-dashboard dependency. In that case you can use the following code:
#GetMapping(value = "/search/{text}")
public Mono<ResponseEntity<List<SearchResult>>> search(#PathVariable(
value = "text") String text) {
return searchService().find(text)
.collectList()
.map(list -> ResponseEntity.ok()
.header("Header-Name", "headervalue")
.body(list));
}
A couple of things to note:
Outer type should be Mono<ResponseEntity<T>>: There is one response for request. If you declare it to be a Flux, Spring will try to deserialize the ResponseEntity as if it was a POJO.
You need to use an operator to transform the Flux into a Mono: collectList() or single() will do the job for you.
Checked with Spring Boot 2.0.3.RELEASE