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/EXAMPLES.md b/EXAMPLES.md index c5731b7a..c6ae33b6 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -65,6 +65,35 @@ 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, timeouts, or interceptors), you can reuse it as the base for the `DefaultHttpClient` via `withClient`: + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + // your shared transport configuration, e.g. timeouts, dispatcher, or 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(); +``` + +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 7c86b457..e7511ee0 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,15 +56,30 @@ OkHttpClient getOkClient() { } private DefaultHttpClient(Builder builder) { - okhttp3.OkHttpClient.Builder clientBuilder = 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(); } @@ -288,6 +304,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. @@ -295,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; @@ -306,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; @@ -327,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; @@ -381,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; @@ -392,12 +413,42 @@ 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; return this; } + /** + * 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 transport configuration should be reused. + * @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..3b000374 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,31 @@ 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