OkHttp & Interceptors

The last lesson built a TrailApiService interface and a Retrofit instance, but Retrofit itself never actually opens a network socket. It generates the request description — method, URL, headers, body — and hands that off to OkHttp, the HTTP client that does the real work: opening connections, writing bytes, and reading the response back. Retrofit handles the type-safe API definition; OkHttp handles the actual network request. This lesson works one layer down, at the OkHttp level.

What an Interceptor Is

An interceptor is a piece of code that sits in the middle of every request OkHttp sends and every response it receives, with the ability to inspect or modify either one before it continues on its way. Interceptors chain together — each one gets a chance to look at (or change) the request, pass it along, then look at (or change) the response coming back. It's the single most useful extension point OkHttp offers, and TrailLog puts it to real use in two specific ways below.

A Logging Interceptor for Development

HttpLoggingInterceptor, from the okhttp3:logging-interceptor artifact, prints full request and response details to Logcat — URL, headers, and body — which is genuinely invaluable while building and debugging TrailLog's networking code (see also Logging & Debugging Tools for Logcat itself):

val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = HttpLoggingInterceptor.Level.BODY
}

Level.BODY logs everything, including the response payload — extremely useful during development, but something you'd typically dial back (or strip out entirely in release builds) once an app ships, since it can print sensitive data straight into device logs.

An Auth Interceptor: Attaching a Bearer Token Automatically

A genuinely common real-world need: TrailLog's backend requires every authenticated request to carry an Authorization header. Rather than remembering to add that header at every single call site in TrailApiService, an interceptor can attach it once, automatically, to every outgoing request:

class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider()
        val original = chain.request()

        val request = if (token != null) {
            original.newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
        } else {
            original
        }

        return chain.proceed(request)
    }
}

tokenProvider is a small lambda supplying the current token on demand, so this interceptor never has to know where the token actually comes from — it just reads whatever value is current at the moment each request goes out. Once Firebase Auth is introduced later in the track, this is exactly where that token gets plugged in.

Building the OkHttpClient and Wiring It Into Retrofit

Both interceptors get attached when building a custom OkHttpClient, which is then passed into Retrofit's builder — tying this lesson directly to the previous one:

val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor { getCurrentAuthToken() })
    .addInterceptor(loggingInterceptor)
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.traillogapp.example.com/")
    .client(okHttpClient)
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
    .build()

Interceptor order matters: as written, the auth header gets attached first, then the logging interceptor runs and prints the request (including that header) to Logcat before it's actually sent.

Timeouts: Why Sensible Defaults Matter on Mobile

connectTimeout caps how long OkHttp waits to establish a connection; readTimeout caps how long it waits for data once connected. Mobile networks are genuinely unreliable — a phone moving from Wi-Fi to cellular, or hiking through an area with a weak signal, is exactly the kind of situation TrailLog's users are actually in. Leaving timeouts unset means OkHttp's own defaults apply (10 seconds for both), but setting them explicitly, as above, makes the behavior a deliberate choice rather than an accident — long enough to tolerate a slow connection, short enough that a hung request doesn't leave the UI stuck indefinitely.

A Brief Note on Response Caching

OkHttp also supports HTTP response caching out of the box — configuring a Cache with a directory and a size limit lets OkHttp honor a server's caching headers and serve some responses from disk without hitting the network again at all. It's worth knowing this exists as a complementary layer to the Room-based caching in TrailRepository, though the deliberate offline-fallback logic built in that repository already covers TrailLog's actual caching needs — this is conceptual awareness for when a project needs HTTP-level caching specifically.

Note: All the interceptor and client-building code above is correct, real Kotlin/OkHttp code — but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.
Up next: The logging interceptor above prints raw JSON to Logcat — JSON & kotlinx.serialization covers how that JSON actually turns into (and back from) real Kotlin objects like Trail.