HTTP response caching with Spring WebClient - java

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?

Related

Get status code of Spring WebClient request

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();

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?

Microservice feign infinite loop of invocations?

I am confused about how an infinite loop of feign calls might behave.
An example:
Assume I have 2 APIs, A & B.
if I call API A, which in turn calls API B via a feign HTTP call, which in turn calls API A again via feign, will it recognize this and break the call chain?
Quick flowchart of calls:
A -> B -> A -> B ... Repeat infinitely?
I have not tried this code, it is just an idea。
But I am assuming that spring-cloud-starter-feign will provide some methods to resolve this problem? Is this assumption correct?
#PostMapping(RestJsonPath.API_A)
ResponseEntity<byte[]> apiA();
#PostMapping(RestJsonPath.API_B)
ResponseEntity<byte[]> apiB();
Will it execute until it times out or hystrix will stop it?
TL;DR:
Feign will keep the connection open on the initial request from A to B until the pre-configured timeout kicks in. At this point, Feign will time out the request and if you have specified a Hystrix fallback, Spring will use your Hystrix fallback as the response.
Explanation:
spring-boot-starter-feign provides an abstraction layer for writing the HTTP request code. It will not handle potential loops or cycles in your code.
Here is an example spring boot feign client from their tutorials website for demonstration:
#FeignClient(value = "jplaceholder",
url = "https://jsonplaceholder.typicode.com/",
configuration = ClientConfiguration.class,
fallback = JSONPlaceHolderFallback.class)
public interface JSONPlaceHolderClient {
#RequestMapping(method = RequestMethod.GET, value = "/posts")
List<Post> getPosts();
#RequestMapping(method = RequestMethod.GET, value = "/posts/{postId}", produces = "application/json")
Post getPostById(#PathVariable("postId") Long postId);
}
Notice first that this is an interface - all the code is auto generated by Spring at startup time, and that code will make RESTful requests to the urls configured via the annotations. For instance, the 2nd request allows us to pass in a path variable, which Spring will ensure makes it on the URL path of the outbound request.
The important thing to stress here is that this interface is only responsible for the HTTP calls, not any potential loops. Logic using this interface (which I can inject to any other Spring Bean as I would any other Spring Bean), is up to you the developer.
Github repo where this example came from.
Spring Boot Docs on spring-boot-starter-openfeign.
Hope this helps you understand the purpose of the openfeign project, and helps you understand that it's up to you to deal with cycles and infinite loops in your application code.
As for Hystrix, that framework comes in to play (if it is enabled) only if one of these generated HTTP requests fails, whether it's a timeout, 4xx error, 5xx error, or a response deserialization error. You configure Hystrix, as a sensible default or fallback for when the HTTP request fails.
This is a decent tutorial on Hystrix.
Some points to call out is that a Hystrix fallback must implement your Feign client interface, and you must specify this class as your Hysterix fallback in the #FeignClient annotation. Spring and Hystrix will call your Hystrix class automatically if a Feign request fails.

Spring Webflux - send data stream to endpoint

I have a question regarding Spring Webflux. I wanted to create a reactive endpoint that consumes content type text/event-stream. Not produce but consume. One of our services needs to send a lot of small objects to another one and we thought that streaming it this way might be a good solution.
#PostMapping(value = "/consumeStream", consumes = MediaType.TEXT_EVENT_STREAM_VALUE)
public Mono<Void> serve(#RequestBody Flux<String> data) {
return data.doOnNext(s -> System.out.println("MessageReceived")).then();
}
I am trying to use Spring WebClient to establish a connection to the endpoint and stream data to it. For example using code:
WebClient.builder().baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector())
.build()
.post()
.uri("/test/serve")
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(BodyInserters.fromPublisher(flux, String.class))
.exchange()
.block();
The flux is a stream that produces a single value every 1 sec.
The problem I have is that the WebClient fully reads the publisher and then sends the data as a whole and not streams it one by one.
Is there anything I can do to do this using this client or any other ? I do not want to go the websockets way.
SSE standard does not allow POST. There is no way to specify method even in browser API https://www.w3.org/TR/eventsource/
Server Side Events as name states are designed for delivering events from the server to the client.

Categories