Interceptors and Client Configuration
What Are Interceptors?
Interceptors are a powerful mechanism to observe, modify, and retry requests. They form a chain: each interceptor processes the request, passes it forward, and can process the response on the way back.
val client = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.addInterceptor(AuthInterceptor { tokenManager.getToken() })
.addNetworkInterceptor(CacheInterceptor())
.build()
Application Interceptors vs Network Interceptors
OkHttp has two interceptor positions:
- Application interceptors: Run before the cache. See the original request. Retries, redirects, and compressions are hidden. Use for logging, auth headers, and request modification.
- Network interceptors: Run after the cache. See the actual request sent over the wire. Use for fine-grained control of headers like
Content-Encoding.
class AuthInterceptor(private val tokenProvider: () -> String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider()
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}
Connection Pooling
OkHttp maintains a connection pool to reuse TCP connections. On mobile, this avoids repeated TLS handshakes.
val client = OkHttpClient.Builder()
.connectionPool(
ConnectionPool(
maxIdleConnections = 5,
keepAliveDuration = 5,
timeUnit = TimeUnit.MINUTES
)
)
.build()
maxIdleConnections controls how many unused connections to keep alive. Too many wastes memory; too few causes frequent reconnects.
Caching and Performance
HTTP Caching in OkHttp
OkHttp supports transparent HTTP caching using Cache-Control headers from the server. When configured, cached responses avoid network calls entirely.
val cache = Cache(
directory = File(context.cacheDir, "http-cache"),
maxSize = 50L * 1024 * 1024 // 50 MB
)
val client = OkHttpClient.Builder()
.cache(cache)
.build()
How Caching Works
- Client makes a request. OkHttp checks the cache for a matching entry.
- If the entry exists and is fresh, it returns immediately without network call.
- If stale, OkHttp sends a conditional request with
If-None-MatchorIf-Modified-Since. - Server responds with 304 (use cache) or 200 (new data).
Cache-Control Directives
| Directive | Meaning |
|---|---|
max-age=3600 |
Cache is fresh for 3600 seconds |
no-cache |
Must revalidate before using cache |
no-store |
Do not cache at all |
must-revalidate |
Must revalidate when stale |
public |
Response may be cached by any cache |
private |
Response is for a single user only |
Offline Caching
For mobile apps that need offline support:
fun buildOfflineRequest(url: String): Request {
return Request.Builder()
.url(url)
.cacheControl(
CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build()
)
.build()
}
The networkBoundResource pattern is standard in Android architecture: return cached data first, fetch fresh data in the background, and fall back to cache on network failure.
Logging and Debugging
HttpLoggingInterceptor
The logging interceptor from OkHttp is essential for debugging. It logs request and response details at different verbosity levels:
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = when {
BuildConfig.DEBUG -> HttpLoggingInterceptor.Level.BODY
else -> HttpLoggingInterceptor.Level.NONE
}
}
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
Warning: Never enable BODY level logging in production. It logs request bodies including passwords and tokens.
Levels Explained
| Level | Logs |
|---|---|
NONE |
Nothing |
BASIC |
Request method, URL, response status, and timing |
HEADERS |
Everything in BASIC plus request and response headers |
BODY |
Everything in HEADERS plus request and response bodies |
Custom Interceptor for Request IDs
For debugging in production, add a unique request ID to every call:
class RequestIdInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestId = UUID.randomUUID().toString()
val request = chain.request().newBuilder()
.addHeader("X-Request-ID", requestId)
.build()
val startNs = System.nanoTime()
val response = chain.proceed(request)
val tookMs = (System.nanoTime() - startNs) / 1_000_000
Log.i("OkHttp", "$requestId ${response.code} in ${tookMs}ms")
return response
}
}
This gives you request tracing across your app without logging sensitive payload data. Combine this with crash reporting tools to correlate network failures with user-reported issues.
Quiz
1. What is the difference between an application interceptor and a network interceptor?
2. What happens if maxIdleConnections is set too low in OkHttp?
3. Why should HttpLoggingInterceptor.Level.BODY never be used in production?
4. What is the purpose of OkHttp's cache?
Flashcards
Question
What is the difference between addInterceptor and addNetworkInterceptor?
Click to reveal answer
Answer
addInterceptor adds an application interceptor that runs before caching. addNetworkInterceptor adds a network interceptor that runs after caching and sees the actual request sent over the wire.
Question
What does OkHttp's connection pool do?
Click to reveal answer
Answer
It reuses TCP connections across requests to the same host. This avoids repeated TLS handshakes, which saves battery and reduces latency on mobile devices.
Question
What HTTP header controls OkHttp caching behavior?
Click to reveal answer
Answer
Cache-Control headers from the server. Directives like max-age, no-cache, and no-store tell OkHttp how long to cache responses and when to revalidate.
Question
Why add a custom X-Request-ID interceptor?
Click to reveal answer
Answer
It adds a unique UUID to every request for tracing. When combined with crash reporting and log aggregation, you can correlate network failures with specific user sessions.
Revision Notes
Key Takeaways
- 1. Application interceptors run before caching, network interceptors after
- 2. Connection pooling reuses TCP connections to avoid TLS handshake overhead
- 3. Never log request bodies in production due to sensitive data exposure
- 4. OkHttp caching follows HTTP Cache-Control headers from the server
Interview Tips
- • Explain the interceptor chain and the two types of interceptors
- • Discuss how connection pooling reduces latency on mobile
- • Describe how you would implement offline caching with OkHttp
- • Know the security risks of verbose logging in production
Cheat Sheet
OkHttp Cheat Sheet
Client Setup:
- Set connect/read/write timeouts (default 10s each)
- Add interceptors for auth, logging, caching
- Configure connection pool for reuse
Interceptors:
- Application: before cache, for auth/logging/modification
- Network: after cache, for fine-grained wire control
- addInterceptor() vs addNetworkInterceptor()
Caching:
- Cache directory in context.cacheDir
- 50MB typical size for mobile
- Server controls via Cache-Control headers
- max-age, no-cache, no-store directives
Logging:
- Use BODY level only in debug builds
- BASIC for staging, NONE for production
- Add X-Request-ID for tracing