spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.51k stars 3.31k forks source link

Reusing RestTemplate in a CompletableFuture ends in looping in filter #3542

Open RomainWilbert opened 4 days ago

RomainWilbert commented 4 days ago

Hello,

I have a GlobalFilter which calls an external URL using RestTemplate.

    private CompletableFuture<String> appelSystemeSecondaire(ServerWebExchange exchange, String path) {
        HttpMethod method = exchange.getRequest().getMethod();
        HttpEntity<String> finalRequest = getRequestEntity(method, exchange);
        return CompletableFuture.supplyAsync(() -> restTemplate.exchange(urlSecondaire + path, method, finalRequest, String.class).getBody())
            .exceptionally(t -> gererErreurAppelSecondaire(t, exchange.getRequest().getPath().toString()));
    }

While everything is fine while running on my computer, when I run the code on K8s I get a strange behavior : it seems the external call ends up in the Filter again, causing an inifinite loop.

A quick fix is to instanciate RestTemplate every time :

 return CompletableFuture.supplyAsync(() -> new RestTemplate.exchange(urlSecondaire + path, method, finalRequest, String.class).getBody())

I have the same behavior using RestClient or WebClient.

Versions used : spring-cloud.version 2023.0.3 spring-boot 3.3.2

spencergibb commented 4 days ago

Nothing you've described makes me think this is a problem with gateway. You've only supplied snippets, not the full filter, so I can't make any comment at all.

RomainWilbert commented 1 day ago

Hi @spencergibb.

Do you require all sources or only the filter and its purpose explained ?

The filter method

```

@Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Avant traitement du flux principal de GW ServerHttpRequest request = exchange.getRequest(); MdcHelper.addHeadersAndPath(request); Date dateActeMetier = new Date();

    String fullPath = gestionPathParam(request);
    logger.debug("Request {} sur le path {}", request.getMethod().name(), fullPath);

    if (estAIgnorer(exchange)) {
        logger.debug("Request sans double run sur le path {}", fullPath);
        return chain.filter(exchange);
    }
    return chain.filter(generateNewExchange(exchange, fullPath, dateActeMetier));
}

    private ServerWebExchange generateNewExchange(ServerWebExchange exchange, String fullPath, Date dateActeMetier) {
        CompletableFuture<String> completableAppelSecondaire = appelSystemeSecondaire(exchange, fullPath);
        ServerHttpResponseDecorator decoratedResponse = getDecoratedResponse(exchange, completableAppelSecondaire, dateActeMetier);
        return exchange.mutate().response(decoratedResponse).build();
    }

The response decorator which call the get method on the completableFuture
@Override
public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {
    // Après traitement du flux principal de GW
    if (body instanceof Flux<? extends DataBuffer> fluxBody) {
        long dureeAppelPrimaire = System.currentTimeMillis() - startTime;
        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
            String reponsePrincipale = decodeAppelPrincipal(dataBuffers);
            String reponse = getReponse(reponsePrincipale, dureeAppelPrimaire);
            traceSiUtilisteurOuService(reponse);
            return exchange.getResponse().bufferFactory().wrap(reponse.getBytes());
        })).onErrorResume(err -> {
            logger.error("error while decorating Response: " + err.getMessage(), err);
            return Mono.empty();
        });
    }
    return super.writeWith(body);
}