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
- Create a
GitLabApi that goes through a proxy (forces the Apache pooling connector):
GitLabApi api = new GitLabApi(url, token, ProxyClientConfig.createProxyClientConfig(proxyUri));
- 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.
AbstractApi.delete(...)returns the JAX-RSResponsewithout reading its entity or closing it. The vast majority of delete callers arevoid(e.g.GroupApi.deleteGroup,ProjectApi.deleteProject,*.deleteHook,*.deleteVariable, …) and discard the returnedResponse. WhenGitLabApiuses the Apache pooling connector (i.e. when configured with a proxy viaProxyClientConfig/ApacheConnectorProvider), aResponsewhose entity is never read or closed keeps its connection leased and never returns it to the pool. GitLab's delete endpoints answer202 Accepted/200 OKwith 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.0and later.6.2.0is not affected - see "Why it regressed" below.Environment
PoolingHttpClientConnectionManageris used)Steps to reproduce
GitLabApithat goes through a proxy (forces the Apache pooling connector):maxPerRoute(default 2) concurrentdeleteGroup/deleteProjectcalls 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 openResponse;validateonly readsresponse.getStatus(). Nothing reads the entity or callsResponse.close(). With the Apache connector, the connection backing aResponseis 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 theClientConfig. 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 theClientConfigRequestScoped, so Jersey rebuilt the runtime (and its connection manager) per request - leaked connections werediscarded along with the per-request client and never accumulated. Since #1312 the
ClientConfigis a singleton and theApacheConnector+PoolingHttpClientConnectionManagerare created once and shared.