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