On this page
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
Application (addInterceptor) | Network (addNetworkInterceptor) | |
|---|---|---|
| Runs | Once per call | Per network request (redirects, retries) |
| Cached responses | Sees them | Not invoked |
| Sees | Request as your code built it | Final request on the wire (with Host and compression headers) |
| Typical use | Auth headers, app logging, metrics | Rewriting cache headers, low-level debugging |
Adding an auth header
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.
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
}
}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
val logging = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
redactHeader("Authorization")
redactHeader("Cookie")
}Common headers and metrics
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
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.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
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.