Skip to content

Commit 2264e8e

Browse files
authored
Refactor DefaultClient from constructor-based to Builder pattern for configurable HTTP client options (#910)
2 parents 6f75ff6 + 57a6798 commit 2264e8e

File tree

7 files changed

+423
-80
lines changed

7 files changed

+423
-80
lines changed

EXAMPLES.md

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,10 +3201,12 @@ The Auth0 class can be configured with a `NetworkingClient`, which will be used
32013201
### Timeout configuration
32023202

32033203
```kotlin
3204-
val netClient = DefaultClient(
3205-
connectTimeout = 30,
3206-
readTimeout = 30
3207-
)
3204+
val netClient = DefaultClient.Builder()
3205+
.connectTimeout(30)
3206+
.readTimeout(30)
3207+
.writeTimeout(30)
3208+
.callTimeout(120)
3209+
.build()
32083210

32093211
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
32103212
account.networkingClient = netClient
@@ -3214,7 +3216,12 @@ account.networkingClient = netClient
32143216
<summary>Using Java</summary>
32153217

32163218
```java
3217-
DefaultClient netClient = new DefaultClient(30, 30);
3219+
DefaultClient netClient = new DefaultClient.Builder()
3220+
.connectTimeout(30)
3221+
.readTimeout(30)
3222+
.writeTimeout(30)
3223+
.callTimeout(120)
3224+
.build();
32183225
Auth0 account = Auth0.getInstance("client id", "domain");
32193226
account.setNetworkingClient(netClient);
32203227
```
@@ -3223,23 +3230,30 @@ account.setNetworkingClient(netClient);
32233230
### Logging configuration
32243231

32253232
```kotlin
3226-
val netClient = DefaultClient(
3227-
enableLogging = true
3228-
)
3233+
val netClient = DefaultClient.Builder()
3234+
.enableLogging(true)
3235+
.build()
32293236

32303237
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
32313238
account.networkingClient = netClient
32323239
```
32333240

3241+
You can also provide a custom logger to control where logs are written:
3242+
3243+
```kotlin
3244+
val netClient = DefaultClient.Builder()
3245+
.enableLogging(true)
3246+
.logger(HttpLoggingInterceptor.Logger { message -> Log.d("Auth0Http", message) })
3247+
.build()
3248+
```
3249+
32343250
<details>
32353251
<summary>Using Java</summary>
32363252

32373253
```java
3238-
import java.util.HashMap;
3239-
3240-
DefaultClient netClient = new DefaultClient(
3241-
10, 10, new HashMap<>() ,true
3242-
);
3254+
DefaultClient netClient = new DefaultClient.Builder()
3255+
.enableLogging(true)
3256+
.build();
32433257
Auth0 account = Auth0.getInstance("client id", "domain");
32443258
account.setNetworkingClient(netClient);
32453259
```
@@ -3248,9 +3262,9 @@ account.setNetworkingClient(netClient);
32483262
### Set additional headers for all requests
32493263

32503264
```kotlin
3251-
val netClient = DefaultClient(
3252-
defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}")
3253-
)
3265+
val netClient = DefaultClient.Builder()
3266+
.defaultHeaders(mapOf("{HEADER-NAME}" to "{HEADER-VALUE}"))
3267+
.build()
32543268

32553269
val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
32563270
account.networkingClient = netClient
@@ -3263,9 +3277,9 @@ account.networkingClient = netClient
32633277
Map<String, String> defaultHeaders = new HashMap<>();
32643278
defaultHeaders.put("{HEADER-NAME}", "{HEADER-VALUE}");
32653279

3266-
DefaultClient netClient = new DefaultClient(
3267-
10,10 , defaultHeaders
3268-
);
3280+
DefaultClient netClient = new DefaultClient.Builder()
3281+
.defaultHeaders(defaultHeaders)
3282+
.build();
32693283
Auth0 account = Auth0.getInstance("client id", "domain");
32703284
account.setNetworkingClient(netClient);
32713285
```

V4_MIGRATION_GUIDE.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ implementation 'com.google.code.gson:gson:2.8.9' // your preferred version
9696

9797
> **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0.
9898
99+
### DefaultClient.Builder
100+
101+
v4 introduces a `DefaultClient.Builder` for configuring the HTTP client. This replaces the constructor-based approach with a more flexible builder pattern that supports additional options such as write/call timeouts, custom interceptors, and custom loggers.
102+
103+
**v3 (constructor-based — deprecated):**
104+
105+
```kotlin
106+
// ⚠️ Deprecated: still compiles but shows a warning
107+
val client = DefaultClient(
108+
connectTimeout = 30,
109+
readTimeout = 30,
110+
enableLogging = true
111+
)
112+
```
113+
114+
**v4 (builder pattern — recommended):**
115+
116+
```kotlin
117+
val client = DefaultClient.Builder()
118+
.connectTimeout(30)
119+
.readTimeout(30)
120+
.writeTimeout(30)
121+
.callTimeout(120)
122+
.enableLogging(true)
123+
.build()
124+
```
125+
126+
The legacy constructor is deprecated but **not removed** — existing code will continue to compile and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to migrate to the Builder.
127+
99128
## Getting Help
100129

101130
If you encounter issues during migration:

auth0/src/main/java/com/auth0/android/request/DefaultClient.kt

Lines changed: 162 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import okhttp3.Headers
99
import okhttp3.Headers.Companion.toHeaders
1010
import okhttp3.HttpUrl
1111
import okhttp3.HttpUrl.Companion.toHttpUrl
12-
import okhttp3.Interceptor
1312
import okhttp3.MediaType
1413
import okhttp3.MediaType.Companion.toMediaType
1514
import okhttp3.OkHttpClient
@@ -24,39 +23,181 @@ import javax.net.ssl.X509TrustManager
2423

2524
/**
2625
* Default implementation of a Networking Client.
26+
*
27+
* Use [DefaultClient.Builder] to create a new instance with custom configuration:
28+
*
29+
* ```kotlin
30+
* val client = DefaultClient.Builder()
31+
* .connectTimeout(30)
32+
* .readTimeout(30)
33+
* .writeTimeout(30)
34+
* .enableLogging(true)
35+
* .build()
36+
* ```
37+
*
38+
* The legacy constructor-based API is still supported for backward compatibility.
2739
*/
28-
public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(
29-
connectTimeout: Int,
30-
readTimeout: Int,
40+
public class DefaultClient private constructor(
3141
private val defaultHeaders: Map<String, String>,
32-
enableLogging: Boolean,
3342
private val gson: Gson,
34-
sslSocketFactory: SSLSocketFactory?,
35-
trustManager: X509TrustManager?
43+
okHttpClientBuilder: OkHttpClient.Builder
3644
) : NetworkingClient {
3745

3846
/**
39-
* Create a new DefaultClient.
47+
* Builder for creating a [DefaultClient] instance with custom configuration.
4048
*
41-
* @param connectTimeout the connection timeout, in seconds, to use when executing requests. Default is ten seconds.
42-
* @param readTimeout the read timeout, in seconds, to use when executing requests. Default is ten seconds.
43-
* @param defaultHeaders any headers that should be sent on all requests. If a specific request specifies a header with the same key as any header in the default headers, the header specified on the request will take precedence. Default is an empty map.
44-
* @param enableLogging whether HTTP request and response info should be logged. This should only be set to `true` for debugging purposes in non-production environments, as sensitive information is included in the logs. Defaults to `false`.
49+
* Example usage:
50+
* ```kotlin
51+
* val client = DefaultClient.Builder()
52+
* .connectTimeout(30)
53+
* .readTimeout(30)
54+
* .writeTimeout(30)
55+
* .callTimeout(60)
56+
* .defaultHeaders(mapOf("X-Custom" to "value"))
57+
* .enableLogging(true)
58+
* .logger(myCustomLogger)
59+
* .build()
60+
* ```
4561
*/
62+
public class Builder {
63+
private var connectTimeout: Int = DEFAULT_TIMEOUT_SECONDS
64+
private var readTimeout: Int = DEFAULT_TIMEOUT_SECONDS
65+
private var writeTimeout: Int = DEFAULT_TIMEOUT_SECONDS
66+
private var callTimeout: Int = 0
67+
private var defaultHeaders: Map<String, String> = mapOf()
68+
private var enableLogging: Boolean = false
69+
private var logger: HttpLoggingInterceptor.Logger? = null
70+
private var gson: Gson = GsonProvider.gson
71+
private var sslSocketFactory: SSLSocketFactory? = null
72+
private var trustManager: X509TrustManager? = null
73+
74+
/**
75+
* Sets the connection timeout, in seconds. Default is 10 seconds.
76+
*/
77+
public fun connectTimeout(timeout: Int): Builder = apply { this.connectTimeout = timeout }
78+
79+
/**
80+
* Sets the read timeout, in seconds. Default is 10 seconds.
81+
*/
82+
public fun readTimeout(timeout: Int): Builder = apply { this.readTimeout = timeout }
83+
84+
/**
85+
* Sets the write timeout, in seconds. Default is 10 seconds.
86+
*/
87+
public fun writeTimeout(timeout: Int): Builder = apply { this.writeTimeout = timeout }
88+
89+
/**
90+
* Sets the call timeout, in seconds. Default is 0 (no timeout).
91+
* This is an overall timeout that spans the entire call: resolving DNS, connecting,
92+
* writing the request body, server processing, and reading the response body.
93+
*/
94+
public fun callTimeout(timeout: Int): Builder = apply { this.callTimeout = timeout }
95+
96+
/**
97+
* Sets default headers to include on all requests. If a specific request specifies
98+
* a header with the same key, the request-level header takes precedence.
99+
*/
100+
public fun defaultHeaders(headers: Map<String, String>): Builder =
101+
apply { this.defaultHeaders = headers }
102+
103+
/**
104+
* Enables or disables HTTP logging. Should only be set to `true` for debugging
105+
* in non-production environments, as sensitive information may be logged.
106+
* Defaults to `false`.
107+
*/
108+
public fun enableLogging(enable: Boolean): Builder = apply { this.enableLogging = enable }
109+
110+
/**
111+
* Sets a custom logger for the HTTP logging interceptor.
112+
* Only takes effect if [enableLogging] is set to `true`.
113+
* If not set, the default [HttpLoggingInterceptor.Logger] (which logs to logcat) is used.
114+
*/
115+
public fun logger(logger: HttpLoggingInterceptor.Logger): Builder =
116+
apply { this.logger = logger }
117+
118+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
119+
internal fun gson(gson: Gson): Builder = apply { this.gson = gson }
120+
121+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
122+
internal fun sslSocketFactory(
123+
sslSocketFactory: SSLSocketFactory,
124+
trustManager: X509TrustManager
125+
): Builder = apply {
126+
this.sslSocketFactory = sslSocketFactory
127+
this.trustManager = trustManager
128+
}
129+
130+
/**
131+
* Builds a new [DefaultClient] instance with the configured options.
132+
*/
133+
public fun build(): DefaultClient {
134+
val okBuilder = OkHttpClient.Builder()
135+
136+
okBuilder.addInterceptor(RetryInterceptor())
137+
138+
if (enableLogging) {
139+
val loggingInterceptor = if (logger != null) {
140+
HttpLoggingInterceptor(logger!!)
141+
} else {
142+
HttpLoggingInterceptor()
143+
}
144+
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
145+
okBuilder.addInterceptor(loggingInterceptor)
146+
}
147+
148+
okBuilder.connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
149+
okBuilder.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
150+
okBuilder.writeTimeout(writeTimeout.toLong(), TimeUnit.SECONDS)
151+
okBuilder.callTimeout(callTimeout.toLong(), TimeUnit.SECONDS)
152+
153+
val ssl = sslSocketFactory
154+
val tm = trustManager
155+
if (ssl != null && tm != null) {
156+
okBuilder.sslSocketFactory(ssl, tm)
157+
}
158+
159+
return DefaultClient(defaultHeaders, gson, okBuilder)
160+
}
161+
}
162+
163+
/**
164+
* Create a new DefaultClient with default configuration.
165+
*
166+
* For more configuration options, use [DefaultClient.Builder].
167+
*
168+
* @param connectTimeout the connection timeout, in seconds. Default is 10 seconds.
169+
* @param readTimeout the read timeout, in seconds. Default is 10 seconds.
170+
* @param defaultHeaders headers to include on all requests. Default is an empty map.
171+
* @param enableLogging whether to log HTTP request/response info. Defaults to `false`.
172+
*/
173+
@Deprecated(
174+
message = "Use DefaultClient.Builder() for more configuration options.",
175+
replaceWith = ReplaceWith(
176+
"DefaultClient.Builder()" +
177+
".connectTimeout(connectTimeout)" +
178+
".readTimeout(readTimeout)" +
179+
".defaultHeaders(defaultHeaders)" +
180+
".enableLogging(enableLogging)" +
181+
".build()"
182+
)
183+
)
46184
@JvmOverloads
47185
public constructor(
48186
connectTimeout: Int = DEFAULT_TIMEOUT_SECONDS,
49187
readTimeout: Int = DEFAULT_TIMEOUT_SECONDS,
50188
defaultHeaders: Map<String, String> = mapOf(),
51189
enableLogging: Boolean = false,
52190
) : this(
53-
connectTimeout,
54-
readTimeout,
55-
defaultHeaders,
56-
enableLogging,
57-
GsonProvider.gson,
58-
null,
59-
null
191+
defaultHeaders = defaultHeaders,
192+
gson = GsonProvider.gson,
193+
okHttpClientBuilder = OkHttpClient.Builder().apply {
194+
addInterceptor(RetryInterceptor())
195+
if (enableLogging) {
196+
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
197+
}
198+
connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
199+
readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
200+
}
60201
)
61202

62203
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -125,35 +266,14 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV
125266
}
126267

127268
init {
128-
val builder = OkHttpClient.Builder()
129-
// Add retry interceptor
130-
builder.addInterceptor(RetryInterceptor())
131-
132-
// logging
133-
if (enableLogging) {
134-
val logger: Interceptor = HttpLoggingInterceptor()
135-
.setLevel(HttpLoggingInterceptor.Level.BODY)
136-
builder.addInterceptor(logger)
137-
}
138-
139-
// timeouts
140-
builder.connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
141-
builder.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
269+
okHttpClient = okHttpClientBuilder.build()
142270

143-
// testing with ssl hook (internal constructor params visibility only)
144-
if (sslSocketFactory != null && trustManager != null) {
145-
builder.sslSocketFactory(sslSocketFactory, trustManager)
146-
}
147-
148-
okHttpClient = builder.build()
149-
150-
// Non-retryable client for DPoP requests
271+
// Non-retryable client for DPoP requests — inherits all configuration
151272
nonRetryableOkHttpClient = okHttpClient.newBuilder()
152273
.retryOnConnectionFailure(false)
153274
.build()
154275
}
155276

156-
157277
internal companion object {
158278
const val DEFAULT_TIMEOUT_SECONDS: Int = 10
159279
val APPLICATION_JSON_UTF8: MediaType =

auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,7 @@ public class WebAuthProviderTest {
11691169

11701170
@Test
11711171
@Throws(Exception::class)
1172+
@Suppress("DEPRECATION")
11721173
public fun shouldResumeLoginWithCustomNetworkingClient() {
11731174
val networkingClient: NetworkingClient = Mockito.spy(DefaultClient())
11741175
val authCallback = mock<Callback<Credentials, AuthenticationException>>()

0 commit comments

Comments
 (0)