Spring WebClient taking extra time to read Response - spring-boot

We are using WebClient to communicate with another service. For the time being (in performance testing) its a mock which returns response after 150 ms.
But in the service, time taken by WebClient is far greater than this. We had set timeout at 250 ms, and in that case we found that less than 1% request where getting timed out. We increased the timeout to check the max time taken by WebClient. In this case we found that latency goes upto 500-600 ms for some requests.
Both the service and mock is in AWS, so there is minimal network latency. We verified it via New Relic too. Our service was getting response in average of about 153 ms.
So the question why WebClient is taking this extra time for some requests. Is this some configuration issue at our end or a known problem in WebClient. How can we solve this?
We are using Spring Boot version 2.2.0.RELEASE with web flux, netty etc being on default versions that are bundled with this spring boot version.
Code that we are using to create WebClient and sending requests:
WebClient webClient;
#PostConstruct
public void init() {
webClient = WebClient.create();
}
public Mono<?> postRequest(final RequestContext requestContext,
final MultiValueMap headers, final MediaType contentType, final MediaType acceptType) {
Mono<?> response;
try {
String url = requestContext.getDownStreamObject().getDownstreamRequestUrl();
URI uri = new URI(url);
long webClientstartTime = System.currentTimeMillis();
response = webClient.post().uri(uri)
.contentType(contentType)
.headers(httpHeaders -> {
if (Objects.nonNull(headers)) {
httpHeaders.addAll(headers);
}
})
.bodyValue(requestContext.getRequest())
.accept(acceptType)
.exchange()
.timeout(Duration.ofMillis(requestContext.getDownStreamObject().getTimeout()))
.doOnSuccess(clientResponse -> log.info("clientResponse.statusCode() : {}, {} , requestId : {}, host : {} ,webClient latency : {}",
clientResponse.statusCode(), clientResponse.toString(),requestContext.getRequestId(), uri.getHost(),
System.currentTimeMillis() - webClientstartTime))
.doOnError(throwable -> {
log.error("clientResponse error :{} , requestId :{}, sessionId: {}, host : {}, webclient latency : {}",
throwable,requestContext.getRequestId(), requestContext.getServiceId(), uri.getHost(), System.currentTimeMillis() - webClientstartTime);
})
.flatMap(clientResponse -> clientResponse.bodyToMono(String.class));
return response;
} catch (Exception ex) {
log.error("Some exception while processing post request for requestId :{}, sessionId: {}. Error: {}",
requestContext.getRequestId(), requestContext.getServiceId(), Utility.exceptionFormatter(ex));
}
return null;
}

Related

"java.util.NoSuchElementException: Context is empty" Exception while making downstream request using WebClient

I am trying to upgrade to spring boot 2.6.6 from 2.2.0.RELEASE. Upon migrating I found that I was getting the below mentioned exception while making downstream calls using webclient. So I started checking with lower versions of spring boot and I found that my implementation is working fine in Spring Boot 2.6.3. But upgrading to spring boot version 2.6.4 I am getting this error.
JDK version: openjdk 17.0.2
Error:
class org.springframework.web.reactive.function.client.WebClientRequestException | Cause : java.util.NoSuchElementException: Context is empty | Exception message : Context is empty; nested exception is java.util.NoSuchElementException: Context is empty | StackTrace : org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:141)
What changed in spring boot 2.6.4 that I am getting this error? And what changes can i make to my code to fix that.
#PostConstruct
public void init() {
webClient = WebClient.create();
}
public Mono<?> postRequest(final String url, final Object request,
final MultiValueMap headers, final MediaType contentType, final MediaType acceptType) {
Mono<?> response;
try {
URI uri = new URI(url);
long webClientStartTime = System.currentTimeMillis();
response = webClient.post().uri(uri)
.contentType(contentType)
.headers(httpHeaders -> {
if (Objects.nonNull(headers)) {
httpHeaders.addAll(headers);
}
})
.bodyValue(request)
.accept(acceptType)
.exchangeToMono(clientResponse -> {
log.info("clientResponse.statusCode(): {}, path: {}, webClient latency: {}",
clientResponse.statusCode(), uri.getPath(), System.currentTimeMillis() - webClientStartTime);
if (!clientResponse.statusCode().is2xxSuccessful()) {
return Mono.error(new BaseException("Not success response received from downstream. HttpCode: " + clientResponse.statusCode()));
}
return clientResponse.bodyToMono(String.class);
})
.timeout(Duration.ofMillis(500))
.doOnError(throwable -> log.error("clientResponse error: {}, path: {}, webclient latency: {}",
throwable, uri.getPath(), System.currentTimeMillis() - webClientStartTime));
return response;
} catch (Exception ex) {
log.error("Some exception while processing post request. Error: {}", ex.getMessage());
}
return null;
}
As suggested by #VioletaGeorgieva, upgrading to Spring boot 2.6.8 has fixed the issue.

Pact consumer test does not successfully mock the spring webclient request using the created pact

I am new to Pact Contract testing and I am trying to create a Pact consumer test to validate a method that calls an api with get request. The api request is made using Spring Webclient.
I am not able to create the webclient object by just providing the Pact mockserver eg.
WebClient webClient = WebClient.builder().baseUrl(mockServer.getUrl()).build();
I am getting the exception java.lang.IllegalStateException: No suitable default ClientHttpConnector found. The explanation I get on the internet for that , is to include reactor-netty-http and I was able to get past this issue when i included that in the POM. But I don't think that is the right solution here because I need the mockserver to respond to the webclient request and it is not. Has anyone dealt with this issue before or am I doing this wrong?
Here is the code snippet:
public RequestResponsePact pactMethod(PactDslWithProvider builder) {
Map<String, String> headers = new HashMap<>();
headers.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return builder.given("Consumer request")
.uponReceiving(" getResource call")
.path("/path")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body(RESPONSE_JSON).toPact();
}
#Test
#PactTestFor(pactMethod = "pactMethod", port = "9999")
public void consumerTest(MockServer mockServer) {
WebClient webClient = WebClient.builder().baseUrl(mockServer.getUrl()).build();
ConsumerServiceClient consumerServiceClient = new ConsumerServiceClient(webClient);
Mono<Data> data = consumerServiceClient.getData();
StepVerifier.create(data)
.assertNext(resp -> {
try {
Value value = resp.getValue();
Assertions.assertFalse( value.isEmpty());
} catch (Exception e) {
log.error("Unable to convert response to Value", e);
Assertions.fail();
}
}).expectComplete()
.verify();
}
The webclient call:
webClient.get()
.uri("/path")
.retrieve()
.onStatus(status -> status == HttpStatus.NOT_FOUND,
res -> Mono.error(new RunTimeException()))
.bodyToMono(clazz);

Socket timeout not working in Rest template third party API call - Spring boot

I am trying to test response-time out by configuring socket time out when third party rest service call. I am calling external web service by Spring Rest Template in my service.
For response timeout testing purpose, the external web service is taking more time which I configured.
I have configured 1600 milliseconds for timeout, but unfortunately I am getting response in more then configured time, around 2500 - 3000 milliseconds.
As per the configuration I should get time out exception.
public ClientHttpRequestFactory getClientHttpRequestFactory(String timeout) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Integer.parseInt(timeout))
.setConnectionRequestTimeout(Integer.parseInt(timeout))
.setSocketTimeout(Integer.parseInt(timeout))
.build();
CloseableHttpClient closeableHttpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.build();
return new HttpComponentsClientHttpRequestFactory(closeableHttpClient);
}
public String milisecTimeout = "1600";
RestTemplate restTemplate = new RestTemplate(appConfig.getClientHttpRequestFactory(milisecTimeout));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Content-Type", "application/json");
httpHeaders.set("Accept", "application/json");
httpHeaders.set("Accept-Charset", "UTF-8");
HttpEntity<String> httpEntity = new HttpEntity<>(request, httpHeaders);
String responseBody = "";
try {
ResponseEntity<String> response = restTemplate.exchange(hostUrl, HttpMethod.POST, httpEntity, String.class);
String statusCode = response.getStatusCodeValue();
String responseBody = response.getBody();
SearchRS searchSdnRS = objectMapper.readValue(responseBody, SearchRS.class);
} catch (Exception ex){
log.error("Error:", ex.getCause());
}
Please correct me if any misunderstanding.
Socket timeout is defined as maximum time of inactivity between two data packets. It's not about total request duration. So in the case you're describing it could well be that the data transfer from server to client started after 1500 milliseconds and lasted 1000–1500 milliseconds.

Spring Webflux Webclient timesout intermittently

I am getting intermittent ReadTimeOut from netty with the below error:
The connection observed an error","logger_name":"reactor.netty.http.client.HttpClientConnect","thread_name":"reactor-http-epoll-3","level":"WARN","level_value":30000,"stack_trace":"io.netty.handler.timeout.ReadTimeoutException: null
One observation we made is this particular endpoint for which we are getting this issue is a POST with no request body. I am now sending a dummy json in body now which the downstream system ignores and now I don't see this error anymore at all.
Below is my code:
protected <T, S Mono<S sendMonoRequest (HttpMethod method,
HttpHeaders headers,
T requestBody,
URI uri, Class < S responseClass)
throws ApiException, IOException {
log.info("Calling {} {} {} {}", method.toString(), uri.toString(), headers.toString(),
mapper.writeValueAsString(requestBody));
WebClient.RequestBodySpec requestBodySpec = getWebClient().method(method).uri(uri);
headers.keySet().stream().forEach(headerKey -> headers.get(headerKey).stream().
forEach(headerValue -> requestBodySpec.header(headerKey, headerValue)));
return requestBodySpec
.body(BodyInserters.fromObject(requestBody != null ? requestBody : ""))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, this::doOn4xxError)
.onStatus(HttpStatus::is5xxServerError, this::doOn5xxError)
.onStatus(HttpStatus::isError, this::doOnError)
.bodyToMono(responseClass);
}
protected WebClient getWebClient () {
HttpClient httpClient = HttpClient.create().tcpConfiguration(
client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
20000).doOnConnected(conn - conn
.addHandlerLast(new ReadTimeoutHandler(20)).addHandlerLast(new WriteTimeoutHandler(20))));
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
return WebClient.builder().clientConnector(connector)
.filter(logResponse())
.build();
}
To resolve the intemrittent timeouts, I have to send a dummy pojo to sendMonoRequest() for request body. Any ideas ?

How to print responseBody with java reactor webclient before deserialization

We are planing to migrate from spring Rest-Template to Reactor-webclient.
With Rest-template we have written custom logging interceptors where we were printing request and response with uniqueId, before desrialization.
Now weblient provide filters, but in filters I can't access responseBody to log it.
We have some third party APIs where they send strings in case of error and some objects in case of success. In this case I can't wait to log response after deserialization, because it will break and we will not be able to log what response we got.
You can try creating a wrapper WebClient which will first log the response and then will deserialize.
The success response will fall on doOnSuccess and the error will fall on onErrorResume.
public <T> Mono<T> get(String url, Map<String, String> headersMap, Class<T> type) {
Mono<T> responseMono = client.get().uri(url).headers((h) -> headersMap.forEach(h::set)).retrieve()
.bodyToMono(type);
return responseMono.doOnSuccess(response -> log.debug("REST GET call to {} is successfull and response is {}",
url,response).onErrorResume(err -> {
log.error("Exception occurred calling get({}): {}", url,
err.getMessage());
return Mono.error(err);
});
}
Here is some sudo code for something I use to test with:
WebClient client = WebClient.create("www.google.com");
ObjectMapper mapper = new ObjectMapper();
client.get()
.retrieve()
.bodyToMono(String.class)
.map(rawBody -> {
try {
return mapper.readValue(rawBody, Response.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("Cannot deserialize string: " + rawBody);
}
});

Resources