Skip to content

Commit 00a3513

Browse files
authored
feat: allow reusing an existing OkHttpClient via DefaultHttpClient.Bu… (#936)
1 parent 0dd42b4 commit 00a3513

5 files changed

Lines changed: 184 additions & 8 deletions

File tree

‎.fernignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ src/test/java/com/auth0/client/mgmt/OAuthTokenSupplierTest.java
7070
src/test/java/com/auth0/client/mgmt/ManagementApiBuilderTest.java
7171
src/test/java/com/auth0/client/mgmt/CustomDomainInterceptorTest.java
7272
src/test/java/com/auth0/client/mgmt/CustomDomainHeaderIntegrationTest.java
73+
src/test/java/com/auth0/net/client/DefaultHttpClientTest.java
7374

7475
# Configuration files from auth0-real
7576
.codecov.yml

‎EXAMPLES.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,35 @@ If the `DefaultHttpClient` does not support your required networking client conf
6565
your own client by implementing the `Auth0HttpClient` interface and providing it to the API client. This is an advanced
6666
use case and should be used only when necessary.
6767

68+
If you already have a configured `OkHttpClient` (for example, one shared across your application with a custom connection
69+
pool, dispatcher, timeouts, or interceptors), you can reuse it as the base for the `DefaultHttpClient` via `withClient`:
70+
71+
```java
72+
OkHttpClient okHttpClient = new OkHttpClient.Builder()
73+
// your shared transport configuration, e.g. timeouts, dispatcher, or a custom interceptor
74+
.build();
75+
76+
Auth0HttpClient httpClient = DefaultHttpClient.newBuilder()
77+
.withClient(okHttpClient)
78+
.build();
79+
80+
AuthAPI auth = AuthAPI.newBuilder("{YOUR_DOMAIN}", "{YOUR_CLIENT_ID}", "{YOUR_CLIENT_SECRET}")
81+
.withHttpClient(httpClient)
82+
.build();
83+
```
84+
85+
When you supply a client this way, **you own its transport configuration**: timeouts, dispatcher, connection pool, cache,
86+
proxy, and any interceptors on it are used as-is. The corresponding `DefaultHttpClient.Builder` transport settings
87+
(`withReadTimeout`, `withConnectTimeout`, `withMaxRequests`, `withMaxRequestsPerHost`, `withProxy`) are ignored in this
88+
case, so your client is never silently overridden — configure those on your own `OkHttpClient` instead.
89+
90+
The SDK's own behavior is always layered on top and cannot be bypassed: the Auth0 telemetry, rate-limit handling, and
91+
logging interceptors are added to your client. These remain configurable via `withTelemetry`, `telemetryEnabled`,
92+
`withMaxRetries`, and `withLogging`.
93+
94+
> If you do not supply a client, nothing changes: the `DefaultHttpClient.Builder` builds and fully configures the
95+
> transport as before.
96+
6897
### Management API
6998

7099
The Management API client uses `ManagementApi` as the main entry point.

‎src/main/java/com/auth0/net/client/DefaultHttpClient.java‎

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
* API clients.
2626
* </p>
2727
* <p>
28-
* For most use cases, usage of this client is recommended. If you have more advanced use cases,
29-
* such as the need to re-use an existing HTTP client, you may consider providing a custom
30-
* implementation of {@link Auth0HttpClient}.
28+
* For most use cases, usage of this client is recommended. To reuse an existing {@link OkHttpClient}
29+
* and its transport configuration, use {@link Builder#withClient(OkHttpClient)}. Only for advanced use
30+
* cases not covered by the {@link Builder} should you provide a custom implementation of
31+
* {@link Auth0HttpClient}.
3132
* </p>
3233
*/
3334
public class DefaultHttpClient implements Auth0HttpClient {
@@ -55,15 +56,30 @@ OkHttpClient getOkClient() {
5556
}
5657

5758
private DefaultHttpClient(Builder builder) {
58-
okhttp3.OkHttpClient.Builder clientBuilder = new okhttp3.OkHttpClient.Builder();
59-
clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS);
60-
clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS);
59+
okhttp3.OkHttpClient.Builder clientBuilder;
60+
if (builder.baseClient != null) {
61+
// A caller-supplied client owns all transport configuration: timeouts, dispatcher, connection
62+
// pool, cache, proxy, and any interceptors already registered on it are used as-is. The transport
63+
// settings on this builder (timeouts, dispatcher, proxy) are intentionally NOT applied here, so
64+
// they never silently override the caller's client. The SDK's behavior interceptors are still
65+
// layered on below.
66+
clientBuilder = builder.baseClient.newBuilder();
67+
} else {
68+
// Default path (unchanged): the SDK builds and fully configures the transport.
69+
clientBuilder = new okhttp3.OkHttpClient.Builder();
70+
clientBuilder.readTimeout(sanitizeTimeout(builder.readTimeout), TimeUnit.SECONDS);
71+
clientBuilder.connectTimeout(sanitizeTimeout(builder.connectTimeout), TimeUnit.SECONDS);
72+
clientBuilder.dispatcher(getDispatcher(builder.maxRequests, builder.maxRequestsPerHost));
73+
configureProxy(clientBuilder, builder.proxyOptions);
74+
}
75+
76+
// SDK behavior is always applied regardless of the transport client, so Auth0 telemetry, rate-limit
77+
// handling, and logging are guaranteed. These interceptors are parameterized by this builder
78+
// (withTelemetry/withLogging/withMaxRetries), which remain effective even when a base client is used.
6179
clientBuilder.addInterceptor(getLoggingInterceptor(builder.loggingOptions));
6280
clientBuilder.addInterceptor(getTelemetryInterceptor(builder.telemetryEnabled, builder.telemetry));
6381
clientBuilder.addInterceptor(getRateLimitInterceptor(builder.maxRetries));
64-
clientBuilder.dispatcher(getDispatcher(builder.maxRequests, builder.maxRequestsPerHost));
6582

66-
configureProxy(clientBuilder, builder.proxyOptions);
6783
this.client = clientBuilder.build();
6884
}
6985

@@ -288,13 +304,15 @@ public static class Builder {
288304
private int maxRetries = 3;
289305
private int maxRequests = 64;
290306
private int maxRequestsPerHost = 5;
307+
private OkHttpClient baseClient;
291308

292309
/**
293310
* Sets the value of the read timeout, in seconds. Defaults to ten seconds. A value of zero results in no read timeout.
294311
* Negative numbers will be treated as zero.
295312
*
296313
* @param readTimeout the value of the read timeout to use.
297314
* @return this builder instance.
315+
* @see #withClient(OkHttpClient) ignored when a base client is supplied.
298316
*/
299317
public Builder withReadTimeout(int readTimeout) {
300318
this.readTimeout = readTimeout;
@@ -306,6 +324,7 @@ public Builder withReadTimeout(int readTimeout) {
306324
* Negative numbers will be treated as zero.
307325
* @param connectTimeout the value of the connect timeout to use.
308326
* @return this builder instance.
327+
* @see #withClient(OkHttpClient) ignored when a base client is supplied.
309328
*/
310329
public Builder withConnectTimeout(int connectTimeout) {
311330
this.connectTimeout = connectTimeout;
@@ -327,6 +346,7 @@ public Builder withLogging(LoggingOptions loggingOptions) {
327346
*
328347
* @param proxyOptions the Proxy configuration options
329348
* @return this builder instance.
349+
* @see #withClient(OkHttpClient) ignored when a base client is supplied.
330350
*/
331351
public Builder withProxy(ProxyOptions proxyOptions) {
332352
this.proxyOptions = proxyOptions;
@@ -381,6 +401,7 @@ public Builder withMaxRetries(int maxRetries) {
381401
*
382402
* @param maxRequests the number of requests to execute concurrently. Must be equal to or greater than one.
383403
* @return this builder instance.
404+
* @see #withClient(OkHttpClient) ignored when a base client is supplied.
384405
*/
385406
public Builder withMaxRequests(int maxRequests) {
386407
this.maxRequests = maxRequests;
@@ -392,12 +413,42 @@ public Builder withMaxRequests(int maxRequests) {
392413
*
393414
* @param maxRequestsPerHost the maximum number of requests for each host to execute concurrently. Must be equal to or greater than one.
394415
* @return this builder instance.
416+
* @see #withClient(OkHttpClient) ignored when a base client is supplied.
395417
*/
396418
public Builder withMaxRequestsPerHost(int maxRequestsPerHost) {
397419
this.maxRequestsPerHost = maxRequestsPerHost;
398420
return this;
399421
}
400422

423+
/**
424+
* Use an existing {@link OkHttpClient} as the base for this client, reusing all of its
425+
* <strong>transport</strong> configuration as-is: timeouts, dispatcher, connection pool, cache,
426+
* proxy, and any interceptors already registered on it.
427+
* <p>
428+
* When a base client is provided, the transport-related settings on this builder
429+
* ({@link #withReadTimeout(int)}, {@link #withConnectTimeout(int)}, {@link #withMaxRequests(int)},
430+
* {@link #withMaxRequestsPerHost(int)}, and {@link #withProxy(ProxyOptions)}) are
431+
* <strong>ignored</strong>. This is deliberate: your client's configuration is never silently
432+
* overridden. Configuring those concerns is your responsibility, on the {@code OkHttpClient} you
433+
* supply.
434+
* </p>
435+
* <p>
436+
* The SDK's own behavior is always layered on top and cannot be bypassed: the Auth0 telemetry,
437+
* rate-limit handling, and logging interceptors are added to your client. These remain configurable
438+
* via {@link #withTelemetry(String, String)}, {@link #telemetryEnabled(boolean)},
439+
* {@link #withMaxRetries(int)}, and {@link #withLogging(LoggingOptions)}, which stay effective even
440+
* when a base client is used.
441+
* </p>
442+
*
443+
* @param baseClient the {@link OkHttpClient} whose transport configuration should be reused.
444+
* @return this builder instance.
445+
*/
446+
public Builder withClient(OkHttpClient baseClient) {
447+
Asserts.assertNotNull(baseClient, "base client");
448+
this.baseClient = baseClient;
449+
return this;
450+
}
451+
401452
/**
402453
* Create a {@code DefaultHttpClient} from this configured builder.
403454
* @return the created {@code DefaultHttpClient}.

‎src/test/java/com/auth0/client/auth/AuthAPITest.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.*;
3838
import java.util.concurrent.CompletableFuture;
3939
import java.util.stream.Collectors;
40+
import okhttp3.OkHttpClient;
4041
import okhttp3.mockwebserver.RecordedRequest;
4142
import org.junit.jupiter.api.AfterEach;
4243
import org.junit.jupiter.api.BeforeEach;
@@ -299,6 +300,31 @@ public void shouldSendCustomTelemetryWhenConfigured() throws Exception {
299300
assertThat(telemetry.get("version").asText(), is("1.2.3"));
300301
}
301302

303+
@Test
304+
public void shouldReuseBaseClientAndPreserveSdkBehavior() throws Exception {
305+
OkHttpClient baseClient = new OkHttpClient.Builder()
306+
.addInterceptor(chain -> chain.proceed(chain.request()
307+
.newBuilder()
308+
.addHeader("X-Custom-Base", "base-client")
309+
.build()))
310+
.build();
311+
312+
AuthAPI customApi = AuthAPI.newBuilder(server.getBaseUrl(), CLIENT_ID, CLIENT_SECRET)
313+
.withHttpClient(
314+
DefaultHttpClient.newBuilder().withClient(baseClient).build())
315+
.build();
316+
317+
Request<UserInfo> request = customApi.userInfo("accessToken");
318+
server.jsonResponse(AUTH_USER_INFO, 200);
319+
request.execute();
320+
321+
RecordedRequest recordedRequest = server.takeRequest();
322+
// The base client's interceptor is carried over via newBuilder(), so the user's config is preserved.
323+
assertThat(recordedRequest.getHeader("X-Custom-Base"), is("base-client"));
324+
// SDK behavior (telemetry) is still layered on top, so integrity is preserved.
325+
assertThat(recordedRequest.getHeader("Auth0-Client"), is(notNullValue()));
326+
}
327+
302328
@Test
303329
public void shouldNestAuth0JavaInTelemetryEnv() throws Exception {
304330
String value = new Telemetry("my-wrapper-sdk", "1.2.3", "auth0-java-9.9.9").getValue();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.auth0.net.client;
2+
3+
import static com.auth0.AssertsUtil.verifyThrows;
4+
import static org.hamcrest.MatcherAssert.assertThat;
5+
import static org.hamcrest.Matchers.is;
6+
import static org.hamcrest.Matchers.not;
7+
8+
import java.util.concurrent.TimeUnit;
9+
import okhttp3.OkHttpClient;
10+
import org.junit.jupiter.api.Test;
11+
12+
public class DefaultHttpClientTest {
13+
14+
@Test
15+
public void shouldReuseBaseClientTransportConfigAndIgnoreBuilderTransportSettings() {
16+
OkHttpClient baseClient =
17+
new OkHttpClient.Builder().readTimeout(42, TimeUnit.SECONDS).build();
18+
baseClient.dispatcher().setMaxRequests(7);
19+
20+
DefaultHttpClient httpClient = DefaultHttpClient.newBuilder()
21+
.withClient(baseClient)
22+
// Transport settings below must be ignored when a base client is supplied.
23+
.withReadTimeout(99)
24+
.withMaxRequests(99)
25+
.build();
26+
27+
OkHttpClient built = httpClient.getOkClient();
28+
assertThat(built.readTimeoutMillis(), is(42_000));
29+
assertThat(built.dispatcher().getMaxRequests(), is(7));
30+
}
31+
32+
@Test
33+
public void shouldApplyBuilderTransportSettingsWhenNoBaseClient() {
34+
DefaultHttpClient httpClient = DefaultHttpClient.newBuilder()
35+
.withReadTimeout(42)
36+
.withMaxRequests(7)
37+
.build();
38+
39+
OkHttpClient built = httpClient.getOkClient();
40+
assertThat(built.readTimeoutMillis(), is(42_000));
41+
assertThat(built.dispatcher().getMaxRequests(), is(7));
42+
}
43+
44+
@Test
45+
public void shouldLayerSdkInterceptorsOnTopOfBaseClientInterceptors() {
46+
OkHttpClient baseClient = new OkHttpClient.Builder()
47+
.addInterceptor(chain -> chain.proceed(chain.request()))
48+
.build();
49+
int baseInterceptorCount = baseClient.interceptors().size();
50+
51+
DefaultHttpClient httpClient =
52+
DefaultHttpClient.newBuilder().withClient(baseClient).build();
53+
54+
// The base client's interceptor plus the three SDK interceptors (logging, telemetry, rate-limit).
55+
assertThat(httpClient.getOkClient().interceptors().size(), is(baseInterceptorCount + 3));
56+
// The original base client is left untouched.
57+
assertThat(
58+
baseClient.interceptors().size(),
59+
is(not(httpClient.getOkClient().interceptors().size())));
60+
}
61+
62+
@Test
63+
public void shouldThrowWhenBaseClientIsNull() {
64+
verifyThrows(
65+
IllegalArgumentException.class,
66+
() -> DefaultHttpClient.newBuilder().withClient(null),
67+
"'base client' cannot be null!");
68+
}
69+
}

0 commit comments

Comments
 (0)