I am new to WebFlux and reactive way of programming. I am using WebClient to call a REST service, when making the call, I need to set a HTTP header, the value for the HTTP header comes from a reactive stream, but the header method on the WebClient only takes String value...
Mono<String> getHeader() {
...
}
webClient
.post()
.uri("/test")
.body(Mono.just(req),req.getClass())
.header("myHeader",getHeader())
...
The above line won't compile, since header method takes an String for second argument. How can I set a header if the value comes from a reactive stream?
You just need to chain getHeader and web client request using flatMap to create reactive flow
return getHeader()
.flatMap(header ->
webClient
.post()
.uri("/test")
.header("myHeader", header)
.body(BodyInserters.fromValue(req))
...
)
Related
Using the flux Webclient, I'm trying to stream a org.springframework.core.io.buffer.DataBuffer file I downloaded from another webclient to a new endpoint.
This endpoint (which I do not control) requires 2 headers to be set on the upload: Content-Length and Content-Range.
If I set them manually as per the hard coded test below it all goes well. I'm unsure where to get started to have these set dynamically as the Flux databuffer gets uploaded.
public Mono<ResourceResponse>uploadFile(URI destination, Flux<DataBuffer> inputFile){
WebClient webClient = WebClient.create();
return webClient.put()
.uri(destination)
// .header(HttpHeaders.CONTENT_LENGTH, "12")
// .header(HttpHeaders.CONTENT_RANGE, "bytes 0-11/12")
.body(BodyInserters.fromDataBuffers(inputFile))
.retrieve()
.bodyToMono(ResourceResponse.class);
}
Should I be doing more on the client to extract these headers?
public Flux<DataBuffer> downloadFile(URI uri) {
return botClient.get().uri(uri)
.accept(MediaType.APPLICATION_OCTET_STREAM)
.retrieve().bodyToFlux(DataBuffer.class);
}
I've been trying to follow the simplest tutorials out there for how to use WebClient, which I understand to be the next greatest thing compared to RestTemplate.
For example, https://www.baeldung.com/spring-5-webclient#4-getting-a-response
So when I try to do the same thing with https://petstore.swagger.io/v2/pet/findByStatus?status=available which is supposed to return some json,
WebClient webClient = WebClient.create();
webClient.get().uri("https://petstore.swagger.io/v2/pet/findByStatus?status=available").exchange().block();
I have absolutely no idea how to proceed from the resultant DefaultClientResponse object. It shouldn't be this convoluted to arrive at the physical response body, but I digress.
How do I get the response body with the code I provided?
In the form you currently have it, and explaining the behaviour..
WebClient webClient = WebClient.create();
webClient.get()
.uri("https://petstore.swagger.io/v2/pet/findByStatus?status=available")
.exchange()
.block();
the block() starts the request by internally synchronously subscribing to the Mono, and returns the resulting ClientResponse. You could also handle this asynchronously by calling subscribe() on the Mono returned by the exchange() method, instead of block().
In this current form, after the block() you now have all the metadata (ie. from the response header) about the response in a ClientResponse object, including the success status. This does not mean that the response body has finished coming through. If you don't care about the response payload, you could confirm the success and leave it at that.
If you further want to look at the response body, you need to convert the response body stream into some class. A this point you can decide whether you want to read everything into a single Mono with bodyToMono or into a stream of objects (Flux) with bodyToFlux, such as in the case where the response is a JSON array that can be parsed into individual separate Java objects.
However, in your case, you just want to see the JSON as-is. So converting to a String is sufficient. You would just use bodyToMono which would return a Mono object.
WebClient webClient = WebClient.create();
String responseJson = webClient.get()
.uri("https://petstore.swagger.io/v2/pet/findByStatus?status=available")
.exchange()
.block()
.bodyToMono(String.class)
.block();
Here you use block() to wait for the response payload to arrive and be parsed into a String, but you could also subscribe to the Mono to receive it reactively when it is complete.
One thing to note is that retrieve() can be used instead of exchange() to shortcut the ClientResponse. In this case you let default behavior handle error responses. Using exchange() puts all the responsibility on the application for responding to error responses on the ClientResponse. Read more in the Javadoc. The retrieve() version would look as follows. No need to block() as you only care about the response data.
WebClient webClient = WebClient.create();
String responseJson = webClient.get()
.uri("https://petstore.swagger.io/v2/pet/findByStatus?status=available")
.retrieve()
.bodyToMono(String.class)
.block();
Here is how you make a request with RestTemplate
String json = new RestTemplate()
.getForEntity("https://petstore.swagger.io/v2/pet/findByStatus?status=available")
.getBody();
Here is how you make a request with requests
import requests
json = requests.get("https://petstore.swagger.io/v2/pet/findByStatus?status=available")
.content
Here is how you make a request with WebClient
String json = WebClient.create()
.get()
.uri("https://petstore.swagger.io/v2/pet/findByStatus?status=available")
.exchange()
.block()
.bodyToMono(String.class)
.block();
I am trying to send an object to the endpoint but I do not understand why I can't do it with .get(), why .post() has to be used? What if the endpoint method takes an object and does something with it and returns an object? I may want to send an object to the endpoint which takes the object as an argument. Is there a way to do it? How to pass a customer object to getCustomer() endpoint.
WebClient.create("http://localhost:8080")
.get()//why this can not be used? why post has to be used?
.uri("client/getCustomer")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(customer)//with .get() body cannot be passed.
.retrieve()
.bodyToMono(Customer.class);
#GET
#Path("/getCustomer")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public Customer getCustomer(Customer customer) {
//do something
return customer;
}
Edited
In GET methods, the data is sent in the URL. just like:
http://www.test.com/users/1
In POST methods, The data is stored in the request body of the
HTTP request.
Therefore we should not expect .get() method to have .bodyValue().
Now if you wanna send data using GET method, you should send them in the URL, like below snippet
WebClient.create("http://localhost:8080")
.get()
.uri("client/getCustomer/{customerName}" , "testName")
.retrieve()
.bodyToMono(Customer.class);
Useful Spring webClient sample:
spring 5 WebClient and WebTestClient Tutorial with Examples
Further information about POST and GET
HTTP Request Methods
I'm using WebClient (SpringBoot 2.0.2.RELEASE) to send a POST with SOAP request, but it is missing "Content-Length" header required by the legacy API.
Is it possible to configure WebClient to include "Content-Length" header?
There is an Spring Framework Issue resolved and introduced for EncoderHttpMessageWriter in SpringBoot 2.0.1, but it seems not to work for JAXB.
I tried to use BodyInserters:
webClient.post().body(BodyInserters.fromObject(request)).exchange();
and syncBody:
webClient.post().syncBody(request).exchange();
None of them worked for WebClient. Though, when RestTemplate is used, Content-Length is set and API responds with success
I am struggling with the same problem, as an ugly work-around I am manually serializing the request (JSON in my case) and setting the length (Kotlin code):
open class PostRetrieverWith411ErrorFix(
private val objectMapper: ObjectMapper
) {
protected fun <T : Any> post(webClient: WebClient, body: Any, responseClass: Class<T>): Mono<T> {
val bodyJson = objectMapper.writeValueAsString(body)
return webClient.post()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.contentLength(bodyJson.toByteArray(Charset.forName("UTF-8")).size.toLong())
.syncBody(bodyJson)
.retrieve()
.bodyToMono(responseClass)
}
}
If you apply Sven's colleague(Max) solution like we did you can also adapt it for cases like your body being a custom obj but you have to serialize it once:
String req = objectMapper.writeValueAsString(requestObject)
and passed that to
webClient.syncBody(req)
Keep in mind that with SpringBoot 2.0.3.RELEASE, if you'll pass a String to webClient as a request, it will put as ContentType header MediaType.TEXT_PLAIN and that made our integration with other service to fail. We fixed that by setting specifically content type header like this:
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
WebClient is a streaming client and it's kind of difficult to set the content length until the stream has finished. By then the headers are long gone. If you work with legacy, you can re-use your mono (Mono/Flux can be reused, Java streams not) and check the length.
public void post() {
Mono<String> mono = Mono.just("HELLO WORLDZ");
final String response = WebClient.create("http://httpbin.org")
.post()
.uri("/post")
.header(HttpHeaders.CONTENT_LENGTH,
mono.map(s -> String.valueOf(s.getBytes(StandardCharsets.UTF_8).length)).block())
.body(BodyInserters.fromPublisher(mono, String.class))
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println(response);
}
A colleague (well done Max!) of mine came up with cleaner solution, I added some wrapping code so it can be tested:
Mono<String> my = Mono.just("HELLO WORLDZZ")
.flatMap(body -> WebClient.create("http://httpbin.org")
.post()
.uri("/post")
.header(HttpHeaders.CONTENT_LENGTH,
String.valueOf(body.getBytes(StandardCharsets.UTF_8).length))
.syncBody(body)
.retrieve()
.bodyToMono(String.class));
System.out.println(my.block());
Currently I am trying to use Spring Cloud Gateway(Spring Cloud version: Finchley.M5) in a edge-service, my sample has a Spring Session header(X-AUTH-TOKEN) token based authentication.
In Gateway specific RouteLocator, the authentication works well, because the built-in GlobalFilters are applied on the RouteLocator when it passes request to downstream microservice.
But I want to create a generic RouterFunction to consume some resources of downstream services and aggregate them together in a new edgeservice, the GlobalFileters will not apply on my webclient bean.
#Bean
RouterFunction<ServerResponse> routes(WebClient webClient) {
log.debug("authServiceUrl:{}", this.authServiceUrl);
log.debug("postServiceUrl:{}", this.postServiceUrl);
log.debug("favoriteServiceUrl:{}", this.favoriteServiceUrl);
return route(
GET("/posts/{slug}/favorited"),
(req) -> {
Flux<Map> favorites = webClient
.get()
.uri(favoriteServiceUrl + "/posts/{slug}/favorited", req.pathVariable("slug"))
.retrieve()
.bodyToFlux(Map.class);
Publisher<Map> cb = from(favorites)
.commandName("posts-favorites")
.fallback(Flux.just(Collections.singletonMap("favorited", false)))
.eager()
.build();
return ok().body(favorites, Map.class);
}
)
...
Is there a simple solution to apply Gateway Filter also work on the RouterFunction, thus my header token based authentication can work automatically?
Or there is a simple way to pass current X-AUTH-TOKEN header into the downstream webclient request headers?
In traditional MVC, there is a RequestContext used to get all headers from current request context and pass them to the downstream request in a global filter. Is there an alternative of RequestContext in webflux to read all headers from current request context?
The complete codes is here.