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;