[
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]