From 4e71de379e78a5925432783fc38d5137ad5e282a Mon Sep 17 00:00:00 2001
From: tanya732
Date: Mon, 28 Sep 2026 11:27:39 +0530
Subject: [PATCH 1/4] feat: allow reusing an existing OkHttpClient via
DefaultHttpClient.Builder.withClient
---
EXAMPLES.md | 20 +++++++++++
.../auth0/net/client/DefaultHttpClient.java | 23 ++++++++++++-
.../com/auth0/client/auth/AuthAPITest.java | 34 +++++++++++++++++++
3 files changed, 76 insertions(+), 1 deletion(-)
diff --git a/EXAMPLES.md b/EXAMPLES.md
index c5731b7a..24506bf7 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -65,6 +65,26 @@ If the `DefaultHttpClient` does not support your required networking client conf
your own client by implementing the `Auth0HttpClient` interface and providing it to the API client. This is an advanced
use case and should be used only when necessary.
+If you already have a configured `OkHttpClient` (for example, one shared across your application with a custom connection
+pool, dispatcher, or interceptors), you can reuse it as the base for the `DefaultHttpClient` via `withClient`. The SDK's
+own timeouts and interceptors (telemetry, rate-limit handling, and logging) are layered on top of your client, so those
+behaviors are always applied. Where they overlap, the settings configured on the `DefaultHttpClient` builder take
+precedence.
+
+```java
+OkHttpClient okHttpClient = new OkHttpClient.Builder()
+ // your shared configuration, e.g. a custom interceptor
+ .build();
+
+Auth0HttpClient httpClient = DefaultHttpClient.newBuilder()
+ .withClient(okHttpClient)
+ .build();
+
+AuthAPI auth = AuthAPI.newBuilder("{YOUR_DOMAIN}", "{YOUR_CLIENT_ID}", "{YOUR_CLIENT_SECRET}")
+ .withHttpClient(httpClient)
+ .build();
+```
+
### Management API
The Management API client uses `ManagementApi` as the main entry point.
diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
index 7c86b457..30bdb4d3 100644
--- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java
+++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
@@ -55,7 +55,11 @@ OkHttpClient getOkClient() {
}
private DefaultHttpClient(Builder builder) {
- okhttp3.OkHttpClient.Builder clientBuilder = new okhttp3.OkHttpClient.Builder();
+ // When a base client is supplied via Builder#withClient, derive from it so its configuration
+ // (connection pool, dispatcher, cache, custom interceptors, ...) is preserved. The SDK's own
+ // settings and interceptors below are then layered on top; the settings configured on this
+ // builder take precedence over those on the base client.
+ okhttp3.OkHttpClient.Builder clientBuilder = builder.baseClient != null ? builder.baseClient.newBuilder() : new okhttp3.OkHttpClient.Builder();
clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS);
clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS);
clientBuilder.addInterceptor(getLoggingInterceptor(builder.loggingOptions));
@@ -288,6 +292,7 @@ public static class Builder {
private int maxRetries = 3;
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
+ private OkHttpClient baseClient;
/**
* Sets the value of the read timeout, in seconds. Defaults to ten seconds. A value of zero results in no read timeout.
@@ -398,6 +403,22 @@ public Builder withMaxRequestsPerHost(int maxRequestsPerHost) {
return this;
}
+ /**
+ * Use an existing {@link OkHttpClient} as the base for this client, allowing its configuration
+ * (connection pool, dispatcher, cache, custom interceptors, etc.) to be reused. The SDK's own
+ * timeout and interceptor configuration (telemetry, rate-limit handling, and logging) is layered
+ * on top of the provided client, so those behaviors are always applied. Where they overlap, the
+ * settings configured on this builder take precedence over those on the base client.
+ *
+ * @param baseClient the {@link OkHttpClient} whose configuration should be reused as the base.
+ * @return this builder instance.
+ */
+ public Builder withClient(OkHttpClient baseClient) {
+ Asserts.assertNotNull(baseClient, "base client");
+ this.baseClient = baseClient;
+ return this;
+ }
+
/**
* Create a {@code DefaultHttpClient} from this configured builder.
* @return the created {@code DefaultHttpClient}.
diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java
index 5cfdd37b..b84dde57 100644
--- a/src/test/java/com/auth0/client/auth/AuthAPITest.java
+++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java
@@ -37,6 +37,7 @@
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
+import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -299,6 +300,39 @@ public void shouldSendCustomTelemetryWhenConfigured() throws Exception {
assertThat(telemetry.get("version").asText(), is("1.2.3"));
}
+ @Test
+ public void shouldReuseBaseClientAndPreserveSdkBehavior() throws Exception {
+ OkHttpClient baseClient = new OkHttpClient.Builder()
+ .addInterceptor(chain -> chain.proceed(chain.request()
+ .newBuilder()
+ .addHeader("X-Custom-Base", "base-client")
+ .build()))
+ .build();
+
+ AuthAPI customApi = AuthAPI.newBuilder(server.getBaseUrl(), CLIENT_ID, CLIENT_SECRET)
+ .withHttpClient(
+ DefaultHttpClient.newBuilder().withClient(baseClient).build())
+ .build();
+
+ Request request = customApi.userInfo("accessToken");
+ server.jsonResponse(AUTH_USER_INFO, 200);
+ request.execute();
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ // The base client's interceptor is carried over via newBuilder(), so the user's config is preserved.
+ assertThat(recordedRequest.getHeader("X-Custom-Base"), is("base-client"));
+ // SDK behavior (telemetry) is still layered on top, so integrity is preserved.
+ assertThat(recordedRequest.getHeader("Auth0-Client"), is(notNullValue()));
+ }
+
+ @Test
+ public void shouldThrowWhenBaseClientIsNull() {
+ verifyThrows(
+ IllegalArgumentException.class,
+ () -> DefaultHttpClient.newBuilder().withClient(null),
+ "'base client' cannot be null!");
+ }
+
@Test
public void shouldNestAuth0JavaInTelemetryEnv() throws Exception {
String value = new Telemetry("my-wrapper-sdk", "1.2.3", "auth0-java-9.9.9").getValue();
From 371c2646c438810e739a3e1f24bab5756e81e0ec Mon Sep 17 00:00:00 2001
From: tanya732
Date: Wed, 30 Sep 2026 17:08:41 +0530
Subject: [PATCH 2/4] fix spotless check
---
src/main/java/com/auth0/net/client/DefaultHttpClient.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
index 30bdb4d3..b5614185 100644
--- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java
+++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
@@ -59,7 +59,8 @@ private DefaultHttpClient(Builder builder) {
// (connection pool, dispatcher, cache, custom interceptors, ...) is preserved. The SDK's own
// settings and interceptors below are then layered on top; the settings configured on this
// builder take precedence over those on the base client.
- okhttp3.OkHttpClient.Builder clientBuilder = builder.baseClient != null ? builder.baseClient.newBuilder() : new okhttp3.OkHttpClient.Builder();
+ okhttp3.OkHttpClient.Builder clientBuilder =
+ builder.baseClient != null ? builder.baseClient.newBuilder() : new okhttp3.OkHttpClient.Builder();
clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS);
clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS);
clientBuilder.addInterceptor(getLoggingInterceptor(builder.loggingOptions));
From b4587f3564d274fd8c9b4d8666f4a4919d927a7b Mon Sep 17 00:00:00 2001
From: tanya732
Date: Wed, 30 Sep 2026 20:00:08 +0530
Subject: [PATCH 3/4] update changes
---
EXAMPLES.md | 19 +++--
.../auth0/net/client/DefaultHttpClient.java | 62 ++++++++++++-----
.../com/auth0/client/auth/AuthAPITest.java | 8 ---
.../net/client/DefaultHttpClientTest.java | 69 +++++++++++++++++++
4 files changed, 126 insertions(+), 32 deletions(-)
create mode 100644 src/test/java/com/auth0/net/client/DefaultHttpClientTest.java
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 24506bf7..c6ae33b6 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -66,14 +66,11 @@ your own client by implementing the `Auth0HttpClient` interface and providing it
use case and should be used only when necessary.
If you already have a configured `OkHttpClient` (for example, one shared across your application with a custom connection
-pool, dispatcher, or interceptors), you can reuse it as the base for the `DefaultHttpClient` via `withClient`. The SDK's
-own timeouts and interceptors (telemetry, rate-limit handling, and logging) are layered on top of your client, so those
-behaviors are always applied. Where they overlap, the settings configured on the `DefaultHttpClient` builder take
-precedence.
+pool, dispatcher, timeouts, or interceptors), you can reuse it as the base for the `DefaultHttpClient` via `withClient`:
```java
OkHttpClient okHttpClient = new OkHttpClient.Builder()
- // your shared configuration, e.g. a custom interceptor
+ // your shared transport configuration, e.g. timeouts, dispatcher, or a custom interceptor
.build();
Auth0HttpClient httpClient = DefaultHttpClient.newBuilder()
@@ -85,6 +82,18 @@ AuthAPI auth = AuthAPI.newBuilder("{YOUR_DOMAIN}", "{YOUR_CLIENT_ID}", "{YOUR_CL
.build();
```
+When you supply a client this way, **you own its transport configuration**: timeouts, dispatcher, connection pool, cache,
+proxy, and any interceptors on it are used as-is. The corresponding `DefaultHttpClient.Builder` transport settings
+(`withReadTimeout`, `withConnectTimeout`, `withMaxRequests`, `withMaxRequestsPerHost`, `withProxy`) are ignored in this
+case, so your client is never silently overridden — configure those on your own `OkHttpClient` instead.
+
+The SDK's own behavior is always layered on top and cannot be bypassed: the Auth0 telemetry, rate-limit handling, and
+logging interceptors are added to your client. These remain configurable via `withTelemetry`, `telemetryEnabled`,
+`withMaxRetries`, and `withLogging`.
+
+> If you do not supply a client, nothing changes: the `DefaultHttpClient.Builder` builds and fully configures the
+> transport as before.
+
### Management API
The Management API client uses `ManagementApi` as the main entry point.
diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
index b5614185..764cd871 100644
--- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java
+++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
@@ -25,9 +25,10 @@
* API clients.
*
*
- * For most use cases, usage of this client is recommended. If you have more advanced use cases,
- * such as the need to re-use an existing HTTP client, you may consider providing a custom
- * implementation of {@link Auth0HttpClient}.
+ * For most use cases, usage of this client is recommended. To reuse an existing {@link OkHttpClient}
+ * and its transport configuration, use {@link Builder#withClient(OkHttpClient)}. Only for advanced use
+ * cases not covered by the {@link Builder} should you provide a custom implementation of
+ * {@link Auth0HttpClient}.
*
*/
public class DefaultHttpClient implements Auth0HttpClient {
@@ -55,20 +56,30 @@ OkHttpClient getOkClient() {
}
private DefaultHttpClient(Builder builder) {
- // When a base client is supplied via Builder#withClient, derive from it so its configuration
- // (connection pool, dispatcher, cache, custom interceptors, ...) is preserved. The SDK's own
- // settings and interceptors below are then layered on top; the settings configured on this
- // builder take precedence over those on the base client.
- okhttp3.OkHttpClient.Builder clientBuilder =
- builder.baseClient != null ? builder.baseClient.newBuilder() : new okhttp3.OkHttpClient.Builder();
- clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS);
- clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS);
+ okhttp3.OkHttpClient.Builder clientBuilder;
+ if (builder.baseClient != null) {
+ // A caller-supplied client owns all transport configuration: timeouts, dispatcher, connection
+ // pool, cache, proxy, and any interceptors already registered on it are used as-is. The transport
+ // settings on this builder (timeouts, dispatcher, proxy) are intentionally NOT applied here, so
+ // they never silently override the caller's client. The SDK's behavior interceptors are still
+ // layered on below.
+ clientBuilder = builder.baseClient.newBuilder();
+ } else {
+ // Default path (unchanged): the SDK builds and fully configures the transport.
+ clientBuilder = new okhttp3.OkHttpClient.Builder();
+ clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS);
+ clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS);
+ clientBuilder.dispatcher(getDispatcher(builder.maxRequests, builder.maxRequestsPerHost));
+ configureProxy(clientBuilder, builder.proxyOptions);
+ }
+
+ // SDK behavior is always applied regardless of the transport client, so Auth0 telemetry, rate-limit
+ // handling, and logging are guaranteed. These interceptors are parameterized by this builder
+ // (withTelemetry/withLogging/withMaxRetries), which remain effective even when a base client is used.
clientBuilder.addInterceptor(getLoggingInterceptor(builder.loggingOptions));
clientBuilder.addInterceptor(getTelemetryInterceptor(builder.telemetryEnabled, builder.telemetry));
clientBuilder.addInterceptor(getRateLimitInterceptor(builder.maxRetries));
- clientBuilder.dispatcher(getDispatcher(builder.maxRequests, builder.maxRequestsPerHost));
- configureProxy(clientBuilder, builder.proxyOptions);
this.client = clientBuilder.build();
}
@@ -405,13 +416,26 @@ public Builder withMaxRequestsPerHost(int maxRequestsPerHost) {
}
/**
- * Use an existing {@link OkHttpClient} as the base for this client, allowing its configuration
- * (connection pool, dispatcher, cache, custom interceptors, etc.) to be reused. The SDK's own
- * timeout and interceptor configuration (telemetry, rate-limit handling, and logging) is layered
- * on top of the provided client, so those behaviors are always applied. Where they overlap, the
- * settings configured on this builder take precedence over those on the base client.
+ * Use an existing {@link OkHttpClient} as the base for this client, reusing all of its
+ * transport configuration as-is: timeouts, dispatcher, connection pool, cache,
+ * proxy, and any interceptors already registered on it.
+ *
+ * When a base client is provided, the transport-related settings on this builder
+ * ({@link #withReadTimeout(int)}, {@link #withConnectTimeout(int)}, {@link #withMaxRequests(int)},
+ * {@link #withMaxRequestsPerHost(int)}, and {@link #withProxy(ProxyOptions)}) are
+ * ignored. This is deliberate: your client's configuration is never silently
+ * overridden. Configuring those concerns is your responsibility, on the {@code OkHttpClient} you
+ * supply.
+ *
+ *
+ * The SDK's own behavior is always layered on top and cannot be bypassed: the Auth0 telemetry,
+ * rate-limit handling, and logging interceptors are added to your client. These remain configurable
+ * via {@link #withTelemetry(String, String)}, {@link #telemetryEnabled(boolean)},
+ * {@link #withMaxRetries(int)}, and {@link #withLogging(LoggingOptions)}, which stay effective even
+ * when a base client is used.
+ *
*
- * @param baseClient the {@link OkHttpClient} whose configuration should be reused as the base.
+ * @param baseClient the {@link OkHttpClient} whose transport configuration should be reused.
* @return this builder instance.
*/
public Builder withClient(OkHttpClient baseClient) {
diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java
index b84dde57..3b000374 100644
--- a/src/test/java/com/auth0/client/auth/AuthAPITest.java
+++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java
@@ -325,14 +325,6 @@ public void shouldReuseBaseClientAndPreserveSdkBehavior() throws Exception {
assertThat(recordedRequest.getHeader("Auth0-Client"), is(notNullValue()));
}
- @Test
- public void shouldThrowWhenBaseClientIsNull() {
- verifyThrows(
- IllegalArgumentException.class,
- () -> DefaultHttpClient.newBuilder().withClient(null),
- "'base client' cannot be null!");
- }
-
@Test
public void shouldNestAuth0JavaInTelemetryEnv() throws Exception {
String value = new Telemetry("my-wrapper-sdk", "1.2.3", "auth0-java-9.9.9").getValue();
diff --git a/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java b/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java
new file mode 100644
index 00000000..56c1960b
--- /dev/null
+++ b/src/test/java/com/auth0/net/client/DefaultHttpClientTest.java
@@ -0,0 +1,69 @@
+package com.auth0.net.client;
+
+import static com.auth0.AssertsUtil.verifyThrows;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import java.util.concurrent.TimeUnit;
+import okhttp3.OkHttpClient;
+import org.junit.jupiter.api.Test;
+
+public class DefaultHttpClientTest {
+
+ @Test
+ public void shouldReuseBaseClientTransportConfigAndIgnoreBuilderTransportSettings() {
+ OkHttpClient baseClient =
+ new OkHttpClient.Builder().readTimeout(42, TimeUnit.SECONDS).build();
+ baseClient.dispatcher().setMaxRequests(7);
+
+ DefaultHttpClient httpClient = DefaultHttpClient.newBuilder()
+ .withClient(baseClient)
+ // Transport settings below must be ignored when a base client is supplied.
+ .withReadTimeout(99)
+ .withMaxRequests(99)
+ .build();
+
+ OkHttpClient built = httpClient.getOkClient();
+ assertThat(built.readTimeoutMillis(), is(42_000));
+ assertThat(built.dispatcher().getMaxRequests(), is(7));
+ }
+
+ @Test
+ public void shouldApplyBuilderTransportSettingsWhenNoBaseClient() {
+ DefaultHttpClient httpClient = DefaultHttpClient.newBuilder()
+ .withReadTimeout(42)
+ .withMaxRequests(7)
+ .build();
+
+ OkHttpClient built = httpClient.getOkClient();
+ assertThat(built.readTimeoutMillis(), is(42_000));
+ assertThat(built.dispatcher().getMaxRequests(), is(7));
+ }
+
+ @Test
+ public void shouldLayerSdkInterceptorsOnTopOfBaseClientInterceptors() {
+ OkHttpClient baseClient = new OkHttpClient.Builder()
+ .addInterceptor(chain -> chain.proceed(chain.request()))
+ .build();
+ int baseInterceptorCount = baseClient.interceptors().size();
+
+ DefaultHttpClient httpClient =
+ DefaultHttpClient.newBuilder().withClient(baseClient).build();
+
+ // The base client's interceptor plus the three SDK interceptors (logging, telemetry, rate-limit).
+ assertThat(httpClient.getOkClient().interceptors().size(), is(baseInterceptorCount + 3));
+ // The original base client is left untouched.
+ assertThat(
+ baseClient.interceptors().size(),
+ is(not(httpClient.getOkClient().interceptors().size())));
+ }
+
+ @Test
+ public void shouldThrowWhenBaseClientIsNull() {
+ verifyThrows(
+ IllegalArgumentException.class,
+ () -> DefaultHttpClient.newBuilder().withClient(null),
+ "'base client' cannot be null!");
+ }
+}
From 7035593c65224ee34a8cfd2260d6915d6ee16c56 Mon Sep 17 00:00:00 2001
From: tanya732
Date: Wed, 30 Sep 2026 20:22:52 +0530
Subject: [PATCH 4/4] update fernignore
---
.fernignore | 1 +
src/main/java/com/auth0/net/client/DefaultHttpClient.java | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/.fernignore b/.fernignore
index 3b5964c3..7dd5e97c 100644
--- a/.fernignore
+++ b/.fernignore
@@ -70,6 +70,7 @@ src/test/java/com/auth0/client/mgmt/OAuthTokenSupplierTest.java
src/test/java/com/auth0/client/mgmt/ManagementApiBuilderTest.java
src/test/java/com/auth0/client/mgmt/CustomDomainInterceptorTest.java
src/test/java/com/auth0/client/mgmt/CustomDomainHeaderIntegrationTest.java
+src/test/java/com/auth0/net/client/DefaultHttpClientTest.java
# Configuration files from auth0-real
.codecov.yml
diff --git a/src/main/java/com/auth0/net/client/DefaultHttpClient.java b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
index 764cd871..e7511ee0 100644
--- a/src/main/java/com/auth0/net/client/DefaultHttpClient.java
+++ b/src/main/java/com/auth0/net/client/DefaultHttpClient.java
@@ -312,6 +312,7 @@ public static class Builder {
*
* @param readTimeout the value of the read timeout to use.
* @return this builder instance.
+ * @see #withClient(OkHttpClient) ignored when a base client is supplied.
*/
public Builder withReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
@@ -323,6 +324,7 @@ public Builder withReadTimeout(int readTimeout) {
* Negative numbers will be treated as zero.
* @param connectTimeout the value of the connect timeout to use.
* @return this builder instance.
+ * @see #withClient(OkHttpClient) ignored when a base client is supplied.
*/
public Builder withConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
@@ -344,6 +346,7 @@ public Builder withLogging(LoggingOptions loggingOptions) {
*
* @param proxyOptions the Proxy configuration options
* @return this builder instance.
+ * @see #withClient(OkHttpClient) ignored when a base client is supplied.
*/
public Builder withProxy(ProxyOptions proxyOptions) {
this.proxyOptions = proxyOptions;
@@ -398,6 +401,7 @@ public Builder withMaxRetries(int maxRetries) {
*
* @param maxRequests the number of requests to execute concurrently. Must be equal to or greater than one.
* @return this builder instance.
+ * @see #withClient(OkHttpClient) ignored when a base client is supplied.
*/
public Builder withMaxRequests(int maxRequests) {
this.maxRequests = maxRequests;
@@ -409,6 +413,7 @@ public Builder withMaxRequests(int maxRequests) {
*
* @param maxRequestsPerHost the maximum number of requests for each host to execute concurrently. Must be equal to or greater than one.
* @return this builder instance.
+ * @see #withClient(OkHttpClient) ignored when a base client is supplied.
*/
public Builder withMaxRequestsPerHost(int maxRequestsPerHost) {
this.maxRequestsPerHost = maxRequestsPerHost;