[ 
https://issues.apache.org/jira/browse/HTTPCLIENT-2432?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Alekseev Victor resolved HTTPCLIENT-2432.
-----------------------------------------
    Resolution: Implemented

> 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
>            Priority: Major
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> ResponseEntityProxy#cleanup() invokes two sequential calls on ExecRuntime 
> without a finally guard:
>  
> {code:java}
> private void cleanup() throws IOException {
>     if (this.execRuntime != null) {
>         if (this.execRuntime.isEndpointConnected()) {             
>             this.execRuntime.disconnectEndpoint();   // (A)         
>         }
>         this.execRuntime.discardEndpoint();          // (B)
>     }
> }
> {code}
>  
> (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 remains populated, 
> manager.release(...) is never invoked, and PoolingHttpClientConnectionManager 
> continues to count the connection as leased for the client's entire lifetime.
> 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