Spring Boot - request timeout 504 - spring

I have two Spring Boot REST application they talk with each other.
ProjectA, getTest rest service sometimes it takes a minute. It calls from projectB.
#PostMapping(value = "/test")
public ResponseEntity<Map<byte[], List<String>>> getTest(
#RequestBody ObjectDTO configDto) {
try {
HashMap<byte[], List<String>> testMap =
serviceImpl.test(configDto);
if (!testMap.isEmpty())
return new ResponseEntity<>(testMap, HttpStatus.CREATED);
else return new ResponseEntity<>(testMap, HttpStatus.NO_CONTENT);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
ProjectB which calls the API above.
#PostMapping(value = "/getTest")
#Async
public ResponseEntity<Map<byte[], List<String>>> getTest(
#RequestBody Config config) {
try {
Map<byte[], List<String>> val = serviceImpl.testConfig(config);
return new ResponseEntity<>(val, HttpStatus.CREATED);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
It works locally but when I run on prod it always returns after 6.2s:
upstream request timeout
I have already increased the timeout on the properties with the config below on Project B, but did not work.
server.tomcat.connection-timeout=120000
spring.mvc.async.request-timeout=120000
so the question is how to fix a 504 Gateway Timeout Error
Update:
Fixed by setting a timeout on ambassador .yaml file below.
apiVersion: getambassador.io/v2
kind: Mapping
metadata:
name: ambassador-header-mapping
namespace: x
spec:
prefix: /
timeout_ms: 60000
service: dummyservice:8080
ambassador_id: ambassador-x
headers:
X-Forwarded-Proto: https
Host: dummyhost

It looks like you have additional proxy server which have own timouts config.

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.

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

How to use ReadBodyPredicateFactory to cache payload data

I have Spring Boot microservice, and sending large payload using swagger. At the server I get only 15000 chars and reset 2000 chars are not read.
How can I use ReadBodyPredicateFactory to cache the body message text?
I am using springcloudgateway and added filters. In the filter in apply method I am trying to read the payload json using
DefaultServerRequest serverRequest = new DefaultServerRequest(exchange);
body = serverRequest.bodyToMono(String.class).toFuture().get();
Sometimes it hangs.
I tried with Flux and then i get only half message
Flux body = request.getBody();
body.subscribe(buffer -> {
try {
System.out.println("byte count:" +
buffer.readableByteCount());
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
sb.append(bodyString);
} catch (Exception e) {
e.printStackTrace();
}
Recently, I needed the similar thing in my application and I've found that it can be achieved by Spring Cloud Gateway built-in caching in ServerWebExchangeUtils
Before filters that use request content in some business cases, I created a filter that only forces content caching:
#Component
class CachingRequestBodyFilter extends AbstractGatewayFilterFactory<CachingRequestBodyFilter.Config> {
public CachingRequestBodyFilter() {
super(Config.class);
}
public GatewayFilter apply(final Config config) {
return (exchange, chain) -> ServerWebExchangeUtils.cacheRequestBody(exchange,
(serverHttpRequest) -> chain.filter(exchange.mutate().request(serverHttpRequest).build()));
}
public static class Config {
}
}
In any of the subsequent filters, we can extract the content of the request body, as below:
// some ReadRequestBodyFilter filter
public GatewayFilter apply(final Config config) {
return (exchange, chain) -> {
final var cachedBody = new StringBuilder();
final var cachedBodyAttribute = exchange.getAttribute(CACHED_REQUEST_BODY_ATTR);
if (!(cachedBodyAttribute instanceof DataBuffer)) {
// caching gone wrong error handling
}
final var dataBuffer = (DataBuffer) cachedBodyAttribute;
cachedBody.append(StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()).toString());
final var bodyAsJson = cachedBody.toString();
// some processing
return chain.filter(exchange);
};
}
Then the gateway configuration would look like this:
spring:
cloud:
gateway:
routes:
- [...]
filters:
- CachingRequestBodyFilter
- ReadRequestBodyFilter

How to do content based dynamic routing in zuul?

I am working on a micro-services based architecture which includes Spring-Boot, Eureka, Zuul at key level. My problem is as follows:
Service1 : /api/v1/service1 POST
Service2 : /api/v2/service2 POST
application.yml looks like
zuul:
routes:
service1:
path: /api/v1/**
service2:
path: /api/v1/**
common:
path: /common/endpoint
Also I have written a filter where I am trying to take input via this common endpoint and deduct based on post request where to enroute the request i.e. to service1 or service2. But here I am stuck and nothing seems to work out, even after several google searches and checking out other people's problem I am not yet able to found my solution.
here is how my Filter looks like:
public class CommonEndpointFilter extends ZuulFilter {
#Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
if ((ctx.get("proxy") != null) && ctx.get("proxy").equals("common")) {
return true;
}
return false;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
InputStream in = (InputStream) ctx.get("requestEntity");
if (in == null) {
try {
in = request.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
String body = null;
try {
body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
if (body.indexOf("somethingrelatedtoservice1") != -1) {
//forward the request to Service1: /api/v1/service1
} else if (body.indexOf("somethingrelatedtoservice2") != -1) {
//forward the request to Service2: /api/v1/service2
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
#Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
}
I am not able to figure out how to forward the request further to those services and then obtain response to send back via common endpoint.
What am I doing wrong here ? How can I proceed so I don't have to use hardcoded url of any of the service. Please guide me through this.
I have tried out some of the things like:
1. using DiscoveryClient to obtain instance and use setRouteHost method of context. It always throws a zuulfilter exception URL is not proper common/endpoint is appended after the url of service obtained via DiscoveryClient.
2. I tried stupidest thing that came to mind, using RestTemplate to make a request and obtain response and put that in context response which didn't work either, however request was forwarded to the service but I wouldn't receive any response.
Any help is appreciated!!

Resources