Get status code of Spring WebClient request - java

My goal is to get the HttpStatus from a Spring WebClient request. All other information of the request is irrelevant.
My old way of doing this was:
WebClient.builder().baseUrl("url").build()
.get().exchange().map(ClientResponse::statusCode).block();
With Spring Boot 2.4.x/Spring 5.3, the exchange method of WebClient is deprecated in favor of retrieve.
With the help of https://stackoverflow.com/a/65791800 i managed to create the following solution.
WebClient.builder().baseUrl("url").build()
.get().retrieve()
.toEntity(String.class)
.flatMap(entity -> Mono.just(entity.getStatusCode()))
.block();
However, this looks like a very "hacky" way and and does not work when 4xx or 5xx responses are received (In this case the result is null).

This problem can be solved with the method exchangeToMono. This results into the following snippet.
WebClient.builder().baseUrl("url").build()
.get().exchangeToMono(response -> Mono.just(response.statusCode()))
.block();
The usage of the retrieve method can be improved in the following way, still not very clean.
WebClient.builder().baseUrl("url").build()
.get().retrieve()
.onStatus(httpStatus -> true, clientResponse -> {
//Set variable to return here
status.set(clientResponse.statusCode());
return null;
})
.toBodilessEntity().block();

Related

HTTP response caching with Spring WebClient

I am trying to figure out how to cache responses based on the Cache-Control (and related) headers with Spring WebClient. I am aware that caching and a reactive approach do not go naturally hand in hand and that one should use in memory caching.
I see three option that are applicable:
Option 1:
Project Reactor provides various cache methods that can be applied on a Mono:
return webClient.get()
.uri("/hello")
.retrieve()
.bodyToMono(HelloWorld.class)
.cache(Duration.of(3600, ChronoUnit.SECONDS))
.block();
As the caching happens after the ResponseSpec is turned into the Mono of the entity I loose all information from the headers. There is the option to use toEntity instead:
return webClient.get()
.uri("/hello")
.retrieve()
.toEntity(HelloWorld.class)
.cache(Duration.of(3600, ChronoUnit.SECONDS))
.block().getBody();
But I cannot figure out how to access the ResponseEntity from within the cache method. Would something like this work?
return webClient.get()
.uri("/hello")
.retrieve()
.toEntity(HelloWorld.class)
.flatMap(response ->
Mono.just(response).cache(getTtlFromHeader(response.getHeaders()))
)
.block().getBody();
Based on the JavaDoc of cache I gather:
ttlForValue - the TTL-generating Function invoked when source is valued: Should be cached as the response
ttlForError - the TTL-generating Function invoked when source is erroring: Should not be cached
ttlForEmpty - the TTL-generating Supplier invoked when source is empty: Nothing can be cached anyway
With any of the alternatives based on the Mono I do not have a handle to reevaluate if the cached response is still valid.
Option 2:
Implement the caching functionality through a filter. This means that the cached object is the Mono of the response instead of the response itself. While I have not tried this it seems to me that is not optimal, as the Mono is resolved after the first call, and would need to be resolved even when retrieved from the cache. It is also not that clear how to handle the need to reevaluate cached data. This leaves me with the impression, that while it is possible to achieve it through a filter it would be quite a complex one.
Option 3:
Handle the caching outside of the WebClient call. As in my case all calls invoke a blocking subscriber so that the surrounding logic is non-reactive, the caching could be handled with Spring caching given that the ResponseEntity is returned instead of just the response object.
I dislike this idea as the ResponseEntity escapes the WebClient call and I want to encapsulate everything that is specific to the call in there.
Option 4:
As I am using an Apache Async client as the underlying client connector I could switch from:
private CloseableHttpAsyncClient httpClient(int maxTotalConnections, int maxConnectionsPerRoute) {
return HttpAsyncClients.custom()
.setConnectionManager(connectionManager(maxTotalConnections, maxConnectionsPerRoute))
.disableCookieManagement()
.evictExpiredConnections()
.disableRedirectHandling()
.build();
}
to
private CloseableHttpAsyncClient cachableHttpClient(int maxTotalConnections, int maxConnectionsPerRoute) {
CacheConfig cacheConfig = CacheConfig.DEFAULT;
return CachingHttpAsyncClients.custom()
.setCacheConfig(cacheConfig)
.setConnectionManager(connectionManager(maxTotalConnections, maxConnectionsPerRoute))
.disableCookieManagement()
.evictExpiredConnections()
.disableRedirectHandling()
.build();
}
Probably with a bit more looking into the CacheConfig.
This seems the best solution, however verifying that it works through automated tests will be very hard.
Are there other options that I overlooked?
What is the preferred way to do it?

What is the best way to make a non-blocking HTTP request using Reactor WebClient and deserialize the response to an object?

I have experience with asynchronous libraries like Vert.x but new to Reactor/WebFlux specifically. I want to expose a single endpoint on a web application that when hit, turns around and calls another web service, parses the response into a Java object, then accesses fields within the object and does something with them. I am using WebClient to make the HTTP call and Jackson ObjectMapper to deserialize it. My code looks roughly like this (note: RequestUtil.safeDeserialize just uses Jackson to parse the string body into an object and returns Optional<Object> which is why I have an additional map step afterwards):
public Mono<String> function(final String param) {
final String encodedRequestBody = RequestUtil.encodeRequestBody(param);
final Mono<Response> responseMono = webClient.post()
.uri("/endpoint")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + basicAuthHeader)
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromPublisher(Mono.just(encodedRequestBody), String.class))
.exchange()
.flatMap(clientResponseMono -> clientResponseMono.bodyToMono(String.class))
.map(RequestUtil::safeDeserialize)
.map(resp -> resp.orElseThrow(() -> new RuntimeException("Failed to deserialize Oscar response!")));
responseMono.subscribe(response -> {
// Pull out some of the fields from the `Response` type object and do something with them
});
return responseMono.map(Response::aStringField);
}
After performance testing this code against an identical application that follows the exact same logic, but makes the HTTP call via the blocking Java11 HttpClient class, I see almost no difference between the two -- in fact, the WebClient implementation is slightly less performant than the blocking implementation.
Clearly I made a mistake somewhere either with the code or my mental model of what's going on here, so any help/advice is very appreciated. Thanks!
Edit: Based on the advice in #Toerktumlare's response, I have updated the function to the following:
public Mono<String> function(final String param) {
final Mono<String> encodedRequestBody = RequestUtil.encodeRequestBodyToMono(param);
final Mono<Response> responseMono = webClient.post()
.uri("/endpoint")
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + basicAuthHeader)
.accept(MediaType.APPLICATION_JSON)
.body(encodedRequestBody, String.class)
.retrieve()
.bodyToMono(Response.class);
return responseMono.flatMap(response -> {
final String field = response.field();
// Use `field` to do something that would produce a log message
logger.debug("Field is: {}", field);
return Mono.just(field);
});
}
When running this code, I don't see any logging. This makes me think that the HTTP call isn't actually happening (or completing in time?) because when I use subscribe with the same WebClient code, I can successfully print out fields from the response. What am I missing?
Edit2: This function is being used to serve responses to an endpoint (a few lines of code are omitted for conciseness):
#Bean
public RouterFunction<ServerResponse> routerFunction(ResponseHandler handler) {
return RouterFunctions.route(RequestPredicates.GET("/my/endpoint")
.and(RequestPredicates.accept(MediaType.ALL)), handler::endpoint);
}
public Mono<ServerResponse> endpoint(ServerRequest request) {
// Pull out a comma-separated list from the request
final List<String> params = Arrays.asList(fieldFromRequest.split(","));
// For each param, call function(param), roll into list
List<Mono<String>> results = params.stream()
.map(nonBlockingClient::function)
.collect(Collectors.toList());
// Check to see if any of the requests failed
if (results.size() != params.size()) {
return ServerResponse.status(500).build();
}
logger.debug("Success");
return ServerResponse.ok().build();
}
Most likely the your problem is with the usage of subscribe.
A consumer will subscribe to a producer. Your backend application is a producer which makes the calling client the consumer. Which means, its usually the calling client that should be subscribing not you.
What you are doing now is basically consuming your own production. Which is in a way blocking.
In general you should never subscribe in a webflux application, unless your application for example calls an api and then consumes the response (for instance saves it in a database etc). The one the initiates the call, is in general the subscriber.
I would rewrite the last part and drop the subscribe:
return responseMono.flatMap(response -> {
final string = doSomething(response);
return OscarGuacResponse.aStringField(string);
});
Also i see that in RequestUtil::safeDeserializez you return an Optional<T> i would instead look into returning either a Mono#empty, or Mono#error as the return type to be able to use the many error operators that are available in webflux for instance switchIfEmpty, onErrorContinue, defaultIfEmpty etc etc. There is an entire chapter on error handling in the reactor documentation.
Also maybe look into using flatMap instead of map in many places. To understand the differences between these two you can look at this answer.
Also, when looking at performance later on, you should understand that when you measure webflux performance, you need to look at such things as memory footprint and number of threads, compared to non-blocking applications. You might not see any performance gain when it comes to speed, but instead see that the application uses a lot less threads which in turn means a smaller memory footprint which is a gain itself.
Update:
You are trying to code regular java when doing reactive programming which will not work. Why your code is not working is because you are "breaking the chain".
I wrote this without an IDE, so it might not compile, but you should get the understanding of it. You always need to chain on the previous, and things like java streams etc are usually not needed in reactive programming.
public Mono<ServerResponse> endpoint(ServerRequest request) {
final List<String> params = Arrays.asList(fieldFromRequest.split(","));
return Flux.fromIterable(params)
.flatMap(param -> nonBlockingClient.function(param))
.collectList()
.flatMap(list -> {
if (list.size() != params.size()) {
return ServerResponse.status(500).build();
}
return ServerResponse.ok().build();
})
}
This is basic reactive programming and i HIGHLY suggest you go through the "getting started section" of the reactor documentation so you understand the basics, because if you are going to code regular java in a reactive application, you are going to have a bad time.

Spring WebFlux - a question about duplicating method invocation

I've created a synthetic application using Spring Reactive framework to investigate caching mechanics, that is proposed with Webflux.
What I've noticed, is that when I use a Webclient, that addresses to a third party URL, a method that uses it is called twice, whereas a WebClient, that addresses to my own endpoint is called only one time per request as expected.
I wonder why is it so?
Here is my code for page abstraction, when a webClient is associated with localhost URL, a method getBody() is called only once per request. But when webClient is associated with https://other.size, this method is invoked twice so I see log.info messages two times:
public class Page {
private Mono<String> res;
public Page(WebClient webClient, String url) {
res = webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.cache();
}
public Mono<String> getBody() {
log.info("getting data");
return res;
}
}
Here is a link to the full project: https://github.com/RassulYunussov/webluxmistery
Thanks for the video. It was really helpful.
So if you hit the /tengri endpoint from the browser, you will receive the logs twice and I can confirm I see the same behaviour on my machine.
However, if you hit /tengri using curl you only get the log line once.
Furthermore, looking into the network traffic on the browser I can see a 2nd api call being made to the /tengri endpoint.
Is there some additional logic that will happen when the html is rendered by the browser that will make a 2nd call to /tengri?

Error in REST call using WebClient in Spring Framework

I am trying to make a REST call using WebClient, however I am not able to pass the request body. it is showing error as - the method syncBody(body) is undefined for the type capture #1-of?
public static String getResult(Body body) {
WebClient webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE,MediaType.APPLICATION_JSON_VALUE)
.build();
String result= webClient.get()
.uri(URL)
.syncBody(body)
.retrieve()
.bodyToMono(String.class)
.block();
}
it is showing error as - the method syncBody(body) is undefined for the type capture #1-of?
I may be missing something, but I do not think you can use WebClient. Actually, in spring boot 2.0.5 this is not even compiling.
What happens is that when you invoke .get() you get an instance of RequestHeadersUriSpec that does not have support for body or syncBody methods, while when invoking post (or put, etc) you get an instance of RequestBodyUriSpec that does

ResponseEntity body lost between two webservice calls

I have a webservice which calls another WS and returns the response from the second WS. It looks like so:
// MyController
public ResponseEntity<Foo> requestFooController(#RequestBody #Valid Bar request) {
return this.myService.requestFooService(request);
}
//MyService
ResponseEntity<Foo> requestFooService(Bar request) {
Buzz improvedRequest = ...
return this.secondWS.secondRequestFoo(improvedRequest);
}
When I call the API through Postman, I receive a HTTP OK response with an empty body. Yet, when I'm in debug mode I can see that the service is returning a ResponseEntity with a body. The headers are not lost though.
I changed my code like so and it works fine:
// MyController
public ResponseEntity<Foo> requestFooController(#RequestBody #Valid Bar request) {
ResponseEntity<Foo> tmp = this.myService.requestFooService(request);
return ResponseEntity.status(tmp.getStatusCode()).body(tmp.getBody());
}
Now through Postman I do have the expected body. However, I don't understand the behaviour. I thought that maybe it's due to the fact that the body is some kind of stream that can be read once or something similar. But from reading the source code I don't see anything that could explain this behaviour.
I'm using the Netflix-stack (so HTTP calls between the two WS are made through a Feign client).
Any idea why I'm getting this result?
EDIT:
More details on my stask:
SpringBoot 1.5.3.RELEASE
Feign 2.0.5
There is a bug that causes the named body of an HTTP MultiPart POST to fail. The symptom of this is that you make a POST request with a body, and Spring-Boot can't match it up to an endoint. The exception I see is:
2019-01-23 15:22:45.046 DEBUG 1639 --- [io-8080-exec-10] .w.s.m.m.a.ServletInvocableHandlerMethod : Failed to resolve argument 3 of type 'org.springframework.web.multipart.MultipartFile'
org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present
Zuul is doing caching of the request in order to re-try multiple times. In this process, it fails to preserve the named field for the binary body. You may find it working if you preface the request with zuul. So instead of http://myserver.com/myservice/endpoint use zuul in the path: http://myserver.com/zuul/myservice/endpoint
That will effectively avoid the saving of the request and the retry mechanism.
More details are available on this issue in Zuul's GitHub Bug List.

Categories