Sparround

Retrofit + OkHttp: structure, interceptors, error handling

OkHttp is the HTTP client: connection pooling, TLS, cache, retries, HTTP/2 are its job. Retrofit is a type-safe REST layer on top: it turns interface methods into HTTP calls.

Retrofit's three key parts:

  • The annotated interface@GET, @POST, @Path, @Query, @Body, @Header
  • Converter factories — JSON ↔ object mapping (Gson, Moshi, kotlinx.serialization)
  • Call adapters — the response shape: built-in suspend support, Response<T> for status-code access

Interceptors are OkHttp's most powerful mechanism — every request/response passes through the chain:

  • Application interceptors — called once, unaware of cache/redirects: for auth headers, logging.
  • Network interceptors — at the real network call: they see cache behavior and the actual wire request.
  • Authenticator — invoked on 401: the correct home for token refresh.
kotlin
interface AccountApi {
    @GET("accounts/{id}/balance")
    suspend fun getBalance(@Path("id") accountId: String): BalanceDto

    @POST("transfers")
    suspend fun createTransfer(@Body request: TransferRequest): Response<TransferDto>
}

val client = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .addInterceptor { chain ->                       // application interceptor
        val request = chain.request().newBuilder()
            .header("Authorization", "Bearer ${tokenStore.access()}")
            .build()
        chain.proceed(request)
    }
    .authenticator { _, response ->                  // 401 → token refresh
        val newToken = tokenStore.refreshBlocking() ?: return@authenticator null
        response.request.newBuilder()
            .header("Authorization", "Bearer $newToken")
            .build()
    }
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.bank.example/v2/")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()

A typical Retrofit setup for a bank API

The error-handling interview question: a suspend Retrofit method throws HttpException on 4xx/5xx and IOException on connectivity failures. The professional approach: catch these in the repository and map to a domain result — sealed interface ApiResult { Success; HttpError(code, errorBody); NetworkError }. Do not skip parsing the error body: bank APIs return the decline reason (limit exceeded, card blocked) exactly there. The baseUrl must end with / — otherwise IllegalArgumentException; relative paths must not start with /.

🛠 Practice task

Set up Retrofit against a free test API (e.g. https://httpbin.org):

  • Write an interceptor that adds an auth header and inspect the outgoing request with the logging interceptor.
  • Call httpbin.org/status/500 and /status/404 — catch HttpException; then call with no internet — catch IOException.
  • Write a repository function mapping both into a sealed interface ApiResult.

Done when: no try/catch remains in the ViewModel.

📚 Sources and documentation