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

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.

Related

Spring WebClient taking extra time to read Response

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;
}

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

WireMock gives a null response body when mocking an API call using spring boot resttemplate

I'm facing a weird issue with Wiremock. The code below returns a null response body. Any insight will be very much appreciated.
Stub in my test:
WireMock.stubFor(post(urlPathEqualTo("http://localhost:8080/mapper"))
.willReturn(WireMock.aResponse()
.withStatus(HttpStatus.OK.value())
.withBody(asJson("ct/slotting-response/create_sample_response1.json"))
.withHeader("Content-Type","application/json;charset=UTF-8")));
Actual API call using spring boot resttemplate:
public ResponseEntity<SampleResponse> getsampleValue(final SampleRequest request, RequestHeader requestHeader) throws SlottingException {
try {
log.info("Sending request[payload={}]", request);
final HttpHeaders headers = getRequestHeader(requestHeader);
HttpEntity<?> entity = new HttpEntity<>(request, headers);
final ResponseEntity<SampleResponse> response =
restTemplate.postForEntity("http://localhost:8080/mapper",
entity, SampleResponse.class);
log.info("Sample response {}", response); // response.getBody() gives null
if (HttpStatus.OK.equals(response.getStatusCode())) {
log.info("Sample allocated successfully.");
}
else {
throw new SampleException("failed");
}
return response;
} catch (Exception e) {
throw new SampleException("Failed", e);
}
}
Can someone please point out any obvious mistakes you see in the Wiremock stub?

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

Global Exception handling in Spring reactor and dependent responses

I am trying to create a web application using spring 5 . It's a micro-service which hit few other micro-services. Response from one service is dependent the other.I am using global exception handing in my application.
Here is my code:
#Override
public Mono<Response> checkAvailablity(Request request) {
Mono<Response> authResponse = userService.authenticateToken(request);
return authResponse.doOnSuccess(t -> {
// if success is returned.
// Want to return this innerResponse
Mono<Response> innerResponse =
httpService.sendRequest(Constant.SER_BOOKING_SERVICE_CHECK_AVAILABILTY,
request.toString(), Response.class);
}).doOnError(t -> {
logger.info("Subscribing mono in Booking service - On Error");
Mono.error(new CustomException(Constant.EX_MODULE_CONNECTION_TIMED_OUT));
});
In case of error I want to throw CustomException and catch it in global exception handler:
#ControllerAdvice
public class ExceptionInterceptor {
public static Logger logger = Logger.getLogger(ExceptionInterceptor.class);
#ExceptionHandler(value = CustomException.class)
#ResponseBody
public Response authenticationFailure(ServerHttpRequest httpRequest, ServerHttpResponse response,
CustomException ex) {
logger.info("CustomException Occured with code => " + ex.getMessage());
return buildErrorResponse(ex.getMessage());
}
Based on the above code I have two problems:
The exception which is thrown in Mono.error() is not captured in global exception handler.
In case of success, response from the inner service should be returned.
Used two methods in mono: flatmap() and onErrorMap()
and updated my checkAvailablity() code:
public Mono<Response> checkAvailablity(Request request) {
Mono<Response> authResponse = userService.authenticateToken(request);
return authResponse.flatmap(t -> {
// Added transform() for success case
Mono<Response> response = httpService.sendRequest(Constant.SER_BOOKING_SERVICE_CHECK_AVAILABILTY,
request.toString(), Response.class);
logger.info("Response from SER_BOOKING_SERVICE_CHECK_AVAILABILTY");
return response;
}).onErrorMap(t -> {
// Added onErrorMap() for failure case & now exception is caught in global exception handler.
throw new CustomException(Constant.EX_MODULE_CONNECTION_TIMED_OUT);
});
}

Resources