Alekseev Victor created HTTPCLIENT-2432:
-------------------------------------------
Summary: Connection permanently marked leased when
ResponseEntityProxy.cleanup() fails on a dead socket
Key: HTTPCLIENT-2432
URL: https://issues.apache.org/jira/browse/HTTPCLIENT-2432
Project: HttpComponents HttpClient
Issue Type: Bug
Components: HttpClient (classic)
Affects Versions: 5.6.4
Environment: - org.apache.httpcomponents.client5:httpclient5:5.6.4
- Behavior present since the cleanup() shape was introduced; unchanged in HEAD
at time of filing (please verify against 5.x branch).
Reporter: Alekseev Victor
ResponseEntityProxy#cleanup() invokes two sequential calls on ExecRuntime
without a finally guard:
private void cleanup() throws IOException {
if (this.execRuntime != null) {
if (this.execRuntime.isEndpointConnected()) {
this.execRuntime.disconnectEndpoint(); // (A)
}
this.execRuntime.discardEndpoint(); // (B)
}
}
(A) InternalExecRuntime.disconnectEndpoint() calls endpoint.close() and can
throw IOException when the underlying socket is in a broken state. It does not
touch endpointRef.
(B) InternalExecRuntime.discardEndpoint() is the only path that returns the
lease to the pool — it does endpointRef.getAndSet(null) and, inside a finally,
manager.release(endpoint, null, TimeValue.ZERO_MILLISECONDS).
When (A) throws, (B) is skipped. endpointRef stays populated,
manager.release(...) is never invoked, and PoolingHttpClientConnectionManager
continues to count the connection as leased for the entire lifetime of the
client.
Steps to reproduce
1. Configure a classic CloseableHttpClient with a PoolingHttpClie=N,
maxPerRoute=N).
2. Perform a request against an endpoint that returns a streaming body and
whose transport (proxy, TLS tunnel, load balancer) drops the connection with RST
mid-body — before EOF is reached.
3. Observe PoolTracker/getTotalStats().getLeased() after each such failure.
4. Repeat under a retry stack (application-level, RetryTemplate,
A minimal deterministic reproducer: a stub server that closes theg status line
+ a partial chunk; wrap the client in a loop withretries.
Expected
After the response entity is closed (or cleanup() runs from any of close() /
writeTo() / eofDetected() / streamClosed() / streamAbort()), the leased counter
must return to its pre-request value regardless of whether the gr.
Actual
leased monotonically increases by one every time disconnectEndpoihes maxTotal,
all subsequent lease() calls block untilconnectionRequestTimeout and then fail
with ConnectionRequestTimeoutException. The client is effectively dead for the
remainder of its lifetime.
Impact
- Silent pool exhaustion under any transient network fault that surfaces at
socket close.
- Amplified by any retry layer above the client — one bad tunnel in a single
logical operation.
- No warning is logged; the leaked entries look like healthy leased connections
in JMX/pool stats.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]