Skip to content
intermediate Phase 8 · Networking

OkHttp

Configure OkHttp: interceptors, caching, connection pooling, and logging.

45m
2 problems
Topic Progress 0%

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

  1. Client makes a request. OkHttp checks the cache for a matching entry.
  2. If the entry exists and is fresh, it returns immediately without network call.
  3. If stale, OkHttp sends a conditional request with If-None-Match or If-Modified-Since.
  4. 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?

Question 1 options

2. What happens if maxIdleConnections is set too low in OkHttp?

Question 2 options

3. Why should HttpLoggingInterceptor.Level.BODY never be used in production?

Question 3 options

4. What is the purpose of OkHttp's cache?

Question 4 options

Flashcards

Question

What is the difference between addInterceptor and addNetworkInterceptor?

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?

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?

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?

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