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]

Reply via email to