Skip to content

Connection pool leaks when response not modified #4259

Description

@jpmsilva

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions