๐Ÿ”Œ APIs, Databases, and Backend Integration

OkHttp Interceptors Explained

Interceptors let you inspect and modify every HTTP request and response in one place: auth headers, logging, caching headers, metrics. Used carefully, they remove duplication. Used carelessly, they cause infinite loops and leaked tokens.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. The interceptor chain
  2. Adding an auth header
  3. Token refresh with Authenticator
  4. Logging safely
  5. Common headers and metrics
  6. Wiring it up
  7. Real-world uses
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

Every request needs an Authorization header. Every response should be logged in debug builds. A 401 should trigger a token refresh. Writing that in each Retrofit method would be absurd. Interceptors do it once for every call.

The interceptor chain

OkHttp application and network interceptor chain A request from Retrofit passes through application interceptors such as auth and logging, then OkHttp's internal retry, bridge, cache and connect steps, then network interceptors, and finally reaches the server. The response returns back through the same chain in reverse. Retrofit Applicationauth, logging OkHttp internalsretry ยท cache ยท connect Networksees wire request Server response travels back through the chain
Application interceptors run once per call, even when served from cache. Network interceptors run for each actual network request, including redirects.
Application (addInterceptor)Network (addNetworkInterceptor)
RunsOnce per callPer network request (redirects, retries)
Cached responsesSees themNot invoked
SeesRequest as your code built itFinal request on the wire (with Host and compression headers)
Typical useAuth headers, app logging, metricsRewriting cache headers, low-level debugging

Adding an auth header

AuthInterceptor.ktKotlin
class AuthInterceptor(private val tokens: TokenStore) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        // Skip endpoints that must not carry the token (e.g. login).
        if (original.header("No-Auth") != null) {
            return chain.proceed(original.newBuilder().removeHeader("No-Auth").build())
        }
        val token = tokens.accessToken() ?: return chain.proceed(original)
        return chain.proceed(original.newBuilder().header("Authorization", "Bearer $token").build())
    }
}

Token refresh with Authenticator

For 401 responses, OkHttp's Authenticator is designed for exactly this: it's called with the failed response and may return a new request to retry.

TokenAuthenticator.ktKotlin
class TokenAuthenticator(
    private val tokens: TokenStore,
    private val refreshApi: RefreshApi,          // separate client WITHOUT this authenticator
) : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request? {
        // Give up after one retry to avoid infinite loops.
        if (responseCount(response) >= 2) return null

        synchronized(this) {
            val current = tokens.accessToken()
            val sentToken = response.request.header("Authorization")?.removePrefix("Bearer ")
            // Another thread may have refreshed already: just retry with the new token.
            if (current != null && current != sentToken) {
                return response.request.newBuilder().header("Authorization", "Bearer $current").build()
            }
            val refreshed = runCatching { refreshApi.refreshBlocking(tokens.refreshToken() ?: return null) }
                .getOrNull() ?: run { tokens.clear(); return null }   // refresh failed โ†’ sign out
            tokens.save(refreshed.accessToken, refreshed.refreshToken)
            return response.request.newBuilder().header("Authorization", "Bearer ${refreshed.accessToken}").build()
        }
    }

    private fun responseCount(response: Response): Int {
        var r: Response? = response; var count = 1
        while (r?.priorResponse != null) { count++; r = r.priorResponse }
        return count
    }
}
Authenticator runs on OkHttp's thread

It's synchronous, so the refresh call must be blocking. Use a separate OkHttp client for refresh so the refresh request can't itself trigger the authenticator. Store tokens securely. See Secure Token Storage.

Logging safely

Debug-only logging with redactionKotlin
val logging = HttpLoggingInterceptor().apply {
    level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
    redactHeader("Authorization")
    redactHeader("Cookie")
}

Common headers and metrics

Client info + timingKotlin
class ClientInfoInterceptor(private val appVersion: String, private val metrics: Metrics) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .header("X-App-Version", appVersion)
            .header("Accept-Language", Locale.getDefault().toLanguageTag())
            .build()
        val start = System.nanoTime()
        val response = chain.proceed(request)
        metrics.record(request.url.encodedPath, response.code, (System.nanoTime() - start) / 1_000_000)
        return response
    }
}

Wiring it up

OkHttpClientKotlin
val client = OkHttpClient.Builder()
    .cache(Cache(File(context.cacheDir, "http"), 20L * 1024 * 1024))   // 20 MB HTTP cache
    .addInterceptor(ClientInfoInterceptor(BuildConfig.VERSION_NAME, metrics))
    .addInterceptor(AuthInterceptor(tokenStore))
    .authenticator(TokenAuthenticator(tokenStore, refreshApi))
    .addInterceptor(logging)                                          // last: logs final headers
    .build()

Real-world uses

Multi-tenant APIs

Add a tenant or region header from the current session.

Offline cache control

When offline, rewrite requests to accept stale cached responses. See Caching Strategies.

Debug tools

Debug-only interceptors for mocking endpoints or inspecting traffic.

Common mistakes

  • Refreshing tokens inside an interceptor with the same client, causing recursion and deadlocks.
  • No retry limit on 401, giving infinite loops.
  • Logging tokens or personal data in release builds.
  • Reading the response body in an interceptor without re-creating it. Bodies can be consumed only once, so use peekBody.
  • Blocking calls (DataStore reads) on every request without caching the value in memory.

Frequently asked questions

Should I retry failed requests in an interceptor?

OkHttp already retries some connection failures. Application-level retries with backoff are usually clearer in the repository, where you know whether a request is idempotent.

Can interceptors be suspend functions?

No. The OkHttp interceptor API is synchronous. Keep the work quick, and cache values you need.

How do I test interceptors?

Build an OkHttp client with the interceptor against MockWebServer, enqueue responses, and assert on recorded requests.

Conclusion

Interceptors centralise headers, logging, metrics and cache control, while the Authenticator handles 401 token refresh safely with a retry limit and a separate refresh client. Keep them fast, redact secrets, and order them deliberately.

Next step: turn failures into good UX with Handling API Errors and Network Failures.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp