Skip to content

JAX-RS Client leaked in OAuth token methods (requestJWTUserToken, generateAccessToken, …) — only the Response is closed #315

Description

@wdw-jst

Summary

ApiClient's OAuth token methods build a brand-new JAX-RS Client via buildHttpClient(...) on every invocation but never close it — their finally blocks close only the Response. Each call therefore leaks a Jersey Client (and its underlying connection pool + executor threads + sockets). Long-running services that refresh tokens periodically (e.g. JWT grant per user, on cache expiry) accumulate these until the JVM runs out of heap / threads / file descriptors.

Because the SDK is JAX-RS/Jersey-based, this leak is invisible to tooling that watches OkHttp and only shows up as a steady climb in live org.glassfish.jersey.client.JerseyClient instance counts.

Affected version

Reproduced on 6.2.0 and confirmed still present on master (6.6.0) — so there is currently no released version without the issue.

Affected methods

At least the following in src/main/java/com/docusign/esign/client/ApiClient.java:

  • requestJWTUserToken(...)
  • requestJWTApplicationToken(...) (delegates to requestJWTUserToken)
  • generateAccessToken(...)
  • the deprecated configureJWTAuthorizationFlow(...)

All share this shape:

Client client = buildHttpClient(debugging);
WebTarget target = client.target("https://" + getOAuthBasePath() + "/oauth/token");
...
Response response = null;
try {
  response = invocationBuilder.post(entity);
  ...
} finally {
  try {
    if (response != null) {
      response.close();   // <-- Response is closed
    }
  } catch (Exception e) {
    // it's not critical, ...
  }
  // <-- client is NEVER closed
}

jakarta.ws.rs.client.Client is AutoCloseable and holds a connection pool and a (default async) executor; per the JAX-RS spec it must be closed to release those resources. Since a fresh Client is created per token call and discarded, every token request leaks one.

Expected behavior

Each token method should close the Client it creates (or reuse a single shared instance). E.g.:

Client client = buildHttpClient(debugging);
try {
  ...
} finally {
  if (response != null) {
    try { response.close(); } catch (Exception ignored) { }
  }
  client.close();   // release the Client built for this call
}

(Since these files appear to be Swagger-Codegen-generated, the durable fix probably belongs in the generator template.)

Steps to reproduce

  1. In a long-running JVM, call apiClient.requestJWTUserToken(...) in a loop (e.g. once per simulated token expiry).
  2. Take jmap -histo:live <pid> snapshots over time.
  3. Observe org.glassfish.jersey.client.JerseyClient (and ClientRuntime / ThreadPoolExecutor) instance counts rising monotonically with the number of token calls, never reclaimed even after full GC.

Impact

OutOfMemoryError / thread / file-descriptor exhaustion after long runtime in any service that uses JWT (or auth-code) tokens and refreshes them periodically. Consumers can only work around it by subclassing ApiClient and overriding protected buildHttpClient(boolean) to memoize a single shared Client, since the leaked instance is local to the SDK method and otherwise unreachable.

Environment

  • docusign-esign-java 6.2.0 (also reproduced reading master @ 6.6.0)
  • Jersey / JAX-RS (jakarta.ws.rs) client
  • Java 17

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions