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

WebClient Request/Response bug? #2839

Open mohan-palisetti opened 1 year ago

mohan-palisetti commented 1 year ago

Describe the bug Enabling WebClientHttpRoutingFilter & WebClientWriteResponseFilter leaves gateway client hanging. I can confirm that downstream HTTP calls works fine (200 OK with String Response), gateway logs also confirm 200 OK, but client is left hanging. Disabling WebClientHttpRoutingFilter & WebClientWriteResponseFilter beans (i.e., using default Netty routing & response handling) works just fine. We'd like to use a custom WebClient hence looking to get WebClientWriteResponseFilter working.

Appreciate any pointers. thanks

Sample

@Configuration
public class GatewayFilterConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.builder().build();
    }

    @Bean
    public CustomWebClientRoutingFilter customWebClientRoutingFilter
            (WebClient webClient, ObjectProvider<List<HttpHeadersFilter>> headersFilters) {
        return new CustomWebClientRoutingFilter(webClient, headersFilters);
    }

    @Bean
    public CustomWebClientResponseFilter customWebClientResponseFilter () {
        return new CustomWebClientResponseFilter();
    }
}
public class CustomWebClientRoutingFilter extends WebClientHttpRoutingFilter {
    public CustomWebClientRoutingFilter(WebClient webClient, ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider) {
        super(webClient, headersFiltersProvider);
    }

    @Override
    public int getOrder() {
        return Integer.MAX_VALUE - 1; //Run before the NettyRoutingFilter
    }
}
public class CustomWebClientResponseFilter extends WebClientWriteResponseFilter {
    @Override
    public int getOrder() {
        return Integer.MAX_VALUE;
    }
}
spencergibb commented 1 year ago

Personally, I should have removed them in 4.0. They are not well-tested and should be marked as deprecated. Can you say why you want to use them?

mohan-palisetti commented 1 year ago

well, we have a WebClient with a custom configuration for mTLS and bunch of filters for adding tokens, identity propagation etc. I was hoping to use it as-is instead of unpacking the functionality into gateway.

mohan-palisetti commented 1 year ago

@spencergibb - I think I found the root cause. As per the WebClient documentation when using exchangeToMono or exchangeToFlux, after the returned Mono or Flux completes, the response body is checked and if not consumed it is released to prevent memory and connection leaks. Therefore the response cannot be decoded further downstream. (https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client-exchange)

Changing the below code in WebClientHttpRoutingFilter class

return headersSpec.exchangeToMono(Mono::just)
        // .log("webClient route")
        .flatMap(res -> {
            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().putAll(res.headers().asHttpHeaders());
            response.setStatusCode(res.statusCode());
            // Defer committing the response until all route filters have run
            // Put client response as ServerWebExchange attribute and write
            // response later NettyWriteResponseFilter
            exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
            return chain.filter(exchange);
        });

as shown below seems to have done the trick.

  return headersSpec.exchangeToMono (res -> {
              ServerHttpResponse response = exchange.getResponse();
              response.getHeaders().putAll(res.headers().asHttpHeaders());
              response.setStatusCode(res.statusCode());
              // Save response body as it is released to prevent memory & connection leaks
              exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res.bodyToFlux(DataBuffer.class));
              return chain.filter(exchange);
          });

And of course corresponding changes in WebClientWriteResponseFilter to consume Flux

IMO, it may be useful to make Netty routing/response filter configurable so it can be swapped out with a custom WebClient filters.

Thanks

spencergibb commented 1 year ago

PRs welcome

marnikgr commented 2 months ago

@mohan-palisetti could you please provide the changes you made in WebClientWriteResponseFilter to consume Flux? Thanks!