Skip to content

DELETE calls leak pooled HTTP connections, exhausting the pool (Apache connector / proxy) #1328

Description

@buehlmann

AbstractApi.delete(...) returns the JAX-RS Response without reading its entity or closing it. The vast majority of delete callers are void (e.g. GroupApi.deleteGroup, ProjectApi.deleteProject, *.deleteHook, *.deleteVariable, …) and discard the returned Response. When GitLabApi uses the Apache pooling connector (i.e. when configured with a proxy via ProxyClientConfig/ApacheConnectorProvider), a Response whose entity is never read or closed keeps its connection leased and never returns it to the pool. GitLab's delete endpoints answer 202 Accepted / 200 OK with a body, so each such delete leaks one pooled connection.

With the default pool of 2 connections per route, a handful of deletes exhausts the pool, after which every subsequent request blocks indefinitely in org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking.

Affected versions

6.3.0 and later. 6.2.0 is not affected - see "Why it regressed" below.

Environment

  • gitlab4j-api 6.3.0
  • Client configured with a proxy (so the Jersey Apache connector + PoolingHttpClientConnectionManager is used)
  • Default connection pool (2 per route), no request timeouts

Steps to reproduce

  1. Create a GitLabApi that goes through a proxy (forces the Apache pooling connector):
    GitLabApi api = new GitLabApi(url, token, ProxyClientConfig.createProxyClientConfig(proxyUri));
  2. Issue more than maxPerRoute (default 2) concurrent deleteGroup/deleteProject calls to the same host.

Expected

All deletes complete; connections are returned to the pool and reused.

Actual

Exactly 2 deletes succeed (and hold both pooled connections without releasing them); every further delete blocks forever waiting to lease a connection. The successful deletes really happen on the GitLab side, but their connections are never returned, so the pool stays exhausted.

Root cause

AbstractApi.delete(...)validate(...) returns the open Response; validate only reads response.getStatus(). Nothing reads the entity or calls Response.close(). With the Apache connector, the connection backing a Response is only returned to the pool when the entity is fully read/closed, so the discarded delete responses leak their connections.

GET/POST/PUT do not leak because their callers consume the entity via response.readEntity(...), which releases the connection.

Why it regressed in 6.3.0

PR #1312 ("moved 'property' calls on Jersey APIs") moved the per-request .property() calls onto the ClientConfig. That was a correct performance fix, but as a side effect it changed the lifetime of the connection pool: before, mutating the client per request kept the ClientConfig RequestScoped, so Jersey rebuilt the runtime (and its connection manager) per request - leaked connections were
discarded along with the per-request client and never accumulated. Since #1312 the ClientConfig is a singleton and the ApacheConnector + PoolingHttpClientConnectionManager are created once and shared.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions