spring-bootspring-cloudproject-reactorspring-cloud-gatewayspring-webclient

Spring Cloud Gateway app hangs when synchronous call is made using webclient after 1 minute


Spring cloud Gateway version: 4.1.0

We are developing an API gateway application using Spring Cloud Gateway framework. The application has couple of pre-filters and post-filters implemented as Global ordered filter. We are developing a pre-filter as global filter that will validate the user session token to authenticate the incoming request before routing to the destination endpoint. The user session token is validated using REST API exposed by another microservice. The pre-filter is developed as follows:

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   final String sessionToken = exchange.getRequest().getHeader().getFirst("sessionToken");
   final boolean isValid = restApiConnector.validateSessionToken(exchange, sessionToken) // REST API to validate token invoked using WebClient
     .subscribeOn(Schedulers.boundedElastic())
     .publishOn(Scheduleers.boundedElastic())
     .toFuture().get();  // blocking call
   exchanges.getAttributes().put("sessionStatus", isValid);
   return chain.filter(exchange); 
}

restApiConnector is the connector class that uses Spring WebClient to call REST APIs. validateSessionToken is method that validates supplied token by calling validate session REST API using Spring WebClient.

The call to REST API is intentionally made blocking (synchronous) here since all the subsequent pre-filter are dependent on the outcome of this session validation pre-filter. Also the request will be routed to downstream endpoint only when session token is found to be valid by this pre-filter. Thus, pre-filter must execute before any subsequent pre-filter and downstream endpoint and any post filters. subscribeOn and publishOn are used based on recommendation that long-lived I/O or network calls must be executed in different thread to avoid blocking the main Netty thread.

Above code works fine when request is sent to API gateway and the routing happens post session validation. In case the multiple requests are sent within one minute of last request, then the app works fine. However, if the request is sent after one minutes of last request (idle time), then the request processing gets hang at the REST API invocation and no request is sent to session validation server. API Gateway app at this moment is able to accept subsequent incoming request however all subsequent requests shows same hang behavior.

Please guide on best practice on making synchronous call to network REST API calls in such scenarios.

We have already read multiple articles on the internet and mostly its mentioned to use block() call on the publisher, however it always results in exception for each request so toFuture().get() is used. subscribe() cannot be used here since it will make the REST API invocation asynchronous and there can be chances that REST API might get invoked after the execution of pre-filter and post-filters which is not the expected flow for request processing.


Solution

  • You don't need block or subscribe as this method (filter) already is a part of a reactive flow and returns a Mono.

    Should be something like:

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       final String sessionToken = exchange.getRequest().getHeader().getFirst("sessionToken");
    
       restApiConnector.validateSessionToken(exchange, sessionToken)
           .doOnNext { isValid -> 
               exchanges.getAttributes().put("sessionStatus", isValid)
           } 
           .then(chain.filter(exchange))
    }
    

    Proceed with the chain only if valid:

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
       final String sessionToken = exchange.getRequest().getHeader().getFirst("sessionToken");
    
       restApiConnector.validateSessionToken(exchange, sessionToken)
           .flatMap { isValid ->     
               if (isValid) {
                   chain.filter(exchange)
               } else {
                   ServerResponse.status(UNAUTHORIZED).build()
               }
           } 
    }