This bug was reproduced using Spring Boot 4.1.0, Spring Cloud 2025.1.1, Spring Cloud Gateway Server MVC and Apache HttpComponents Client 5.
We observe that some requests are not properly closed, and therefore not returned to the PoolingHttpClientConnectionManager.
After some debug and correlation, we found that the root cause of the issue is in the org.springframework.cloud.gateway.server.mvc.handler.AbstractGatewayServerResponse#writeTo function.
The problem is in this check (a short circuit logic for unmodified responses):
if (SAFE_METHODS.contains(httpMethod)
&& servletWebRequest.checkNotModified(headers().getETag(), lastModified)) {
return null;
}
else {
return writeToInternal(request, response, context);
}
If the response does not meet the criteria for a Not Modified response (the else branch), it calls writeToInternal, which, among other things, copies the response body, and critically, closes the upstream client response (this happens inside RestClientProxyExchange, where the try-with-resources construct closes the client response. Closing the upstream client response is necessary to return the connection to the pool.
However, if the response meets the criteria for a Not Modified response, then an HTTP 304 Not Modified status is set by the checkNotModified call, and the upstream response is ignored. The bug happens because this branch does not properly close the client response.
We were able to reproduce this with Apache HttpComponents Client 5 (the default choice when this library is present in the classpath). Forcing the use of the JDK HttpClient we were unable to reproduce this issue. I believe that the JDK HttpClient responses are not closeable, and I am not sure how the pooling works with this client.
We did not test for Spring Cloud Gateway Server Webflux.
So a quick workaround is to set:
spring:
http:
clients:
imperative:
factory: jdk
Should anyone desire to keep using the Apache HttpComponents Client 5, the workaround below should also work (we verified it works for us). It simply searches for any client response that may be have been used to handle a proxy call, and closes them.
@Bean
public WebMvcConfigurer cleanupWebMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
Object clientResponse = request.getAttribute(MvcUtils.CLIENT_RESPONSE_ATTR);
if (clientResponse instanceof ClientHttpResponse clientHttpResponse) {
clientHttpResponse.close();
}
}
});
}
};
}
We believe that the short circuit logic in AbstractGatewayServerResponse#writeTo needs to close the client response.
Ideally, this logic should be optional, possibly controllable by a configuration property. The default could be to enable it, to remain a non-breaking change. Generally, we prefer the backend endpoints to have authority over the determination if a response needs to be generated or not, and the gateway proxy to have minimal intervention on those aspects.
This bug was reproduced using Spring Boot 4.1.0, Spring Cloud 2025.1.1, Spring Cloud Gateway Server MVC and Apache HttpComponents Client 5.
We observe that some requests are not properly closed, and therefore not returned to the
PoolingHttpClientConnectionManager.After some debug and correlation, we found that the root cause of the issue is in the
org.springframework.cloud.gateway.server.mvc.handler.AbstractGatewayServerResponse#writeTofunction.The problem is in this check (a short circuit logic for unmodified responses):
If the response does not meet the criteria for a Not Modified response (the else branch), it calls
writeToInternal, which, among other things, copies the response body, and critically, closes the upstream client response (this happens insideRestClientProxyExchange, where the try-with-resources construct closes the client response. Closing the upstream client response is necessary to return the connection to the pool.However, if the response meets the criteria for a Not Modified response, then an HTTP 304 Not Modified status is set by the
checkNotModifiedcall, and the upstream response is ignored. The bug happens because this branch does not properly close the client response.We were able to reproduce this with Apache HttpComponents Client 5 (the default choice when this library is present in the classpath). Forcing the use of the JDK HttpClient we were unable to reproduce this issue. I believe that the JDK HttpClient responses are not closeable, and I am not sure how the pooling works with this client.
We did not test for Spring Cloud Gateway Server Webflux.
So a quick workaround is to set:
Should anyone desire to keep using the Apache HttpComponents Client 5, the workaround below should also work (we verified it works for us). It simply searches for any client response that may be have been used to handle a proxy call, and closes them.
We believe that the short circuit logic in
AbstractGatewayServerResponse#writeToneeds to close the client response.Ideally, this logic should be optional, possibly controllable by a configuration property. The default could be to enable it, to remain a non-breaking change. Generally, we prefer the backend endpoints to have authority over the determination if a response needs to be generated or not, and the gateway proxy to have minimal intervention on those aspects.