Skip to main content

Overview

The network layer provides robust, production-ready components for making secure API calls to the Mercado Libre API. It includes automatic token management, comprehensive error handling, and request/response logging.

SafeApiCallExecutor

Orchestrator for safe network operations that handles exceptions, validates responses, and logs errors.

Purpose

SafeApiCallExecutor is the core component responsible for executing suspended API calls with comprehensive error handling. It follows the Single Responsibility Principle (SRP) by managing:
  • Exception handling via ErrorMapper
  • HTTP response validation via HttpResponseHandler
  • Error logging via NetworkLogger
  • Execution on Dispatchers.IO to avoid blocking the main thread

Implementation

package com.alcalist.tecmeli.core.network.executor

import com.alcalist.tecmeli.core.network.AppError
import com.alcalist.tecmeli.core.network.handler.HttpResponseHandler
import com.alcalist.tecmeli.core.network.logger.NetworkLogger
import com.alcalist.tecmeli.core.network.mapper.ErrorMapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Response

class SafeApiCallExecutor(
    private val responseHandler: HttpResponseHandler,
    private val errorMapper: ErrorMapper,
    private val logger: NetworkLogger
) {
    suspend fun <T, R> execute(
        call: suspend () -> Response<T>,
        transform: (T) -> R
    ): Result<R> = withContext(Dispatchers.IO) {
        try {
            val response = call()
            val result = responseHandler.handleResponse(response, transform)

            if (result.isFailure && response.isSuccessful.not()) {
                val error = result.exceptionOrNull()
                if (error is AppError.Server) {
                    logger.logServerError(error.code, error.message)
                }
            }

            result
        } catch (e: Exception) {
            val appError = errorMapper.mapException(e)
            logger.logUnexpectedError(appError.javaClass.simpleName, e)
            Result.failure(appError)
        }
    }
}

Parameters

responseHandler
HttpResponseHandler
Handler for validating and transforming HTTP responses
errorMapper
ErrorMapper
Mapper for converting exceptions to application errors
logger
NetworkLogger
System for logging technical and server failures

Usage Example

class ProductRepositoryImpl @Inject constructor(
    private val meliApi: MeliApi,
    private val safeApiCallExecutor: SafeApiCallExecutor,
    private val productMapper: ProductMapper
) : ProductRepository {

    override suspend fun searchProducts(query: String): Result<List<Product>> {
        return safeApiCallExecutor.execute(
            call = { meliApi.searchProducts(query) },
            transform = { response -> 
                response.results.map { productMapper.toDomain(it) }
            }
        )
    }
}

Method: execute()

T
Generic Type
The type of the Retrofit response body
R
Generic Type
The desired domain type after transformation
call
suspend () -> Response<T>
Suspended lambda representing the Retrofit API call
transform
(T) -> R
Lambda to map from T to R
Result<R>
Result
Encapsulates success with R or failure with AppError

TokenAuthenticator

OkHttp Authenticator that handles automatic token refresh when the server returns a 401 Unauthorized response.

Purpose

When the server responds with a 401 error, this component intercepts the response and attempts to refresh the access token synchronously via the TokenRepository. If successful, it retries the original request with the new token.

Implementation

package com.alcalist.tecmeli.core.network

import com.alcalist.tecmeli.domain.repository.TokenRepository
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import javax.inject.Inject

class TokenAuthenticator @Inject constructor(
    private val tokenRepository: TokenRepository
) : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.code != 401) return null

        val result = runBlocking {
            tokenRepository.refreshToken()
        }

        return if (result.isSuccess) {
            val newToken = result.getOrNull()
            response.request.newBuilder()
                .header("Authorization", "Bearer $newToken")
                .build()
        } else {
            null
        }
    }
}

Parameters

tokenRepository
TokenRepository
required
Repository for token management and refresh operations

Method: authenticate()

route
Route?
The route that failed
response
Response
The response that caused the authentication challenge (expected 401)
Request?
Request | null
A new Request with the updated token, or null if refresh fails
The authenticator uses runBlocking to perform synchronous token refresh, as required by OkHttp’s Authenticator interface.

AuthInterceptor

OkHttp Interceptor that injects the access token into outgoing HTTP requests.

Purpose

Intercepts every HTTP request directed to the Mercado Libre API and adds the Authorization: Bearer {token} header if a token is available in the TokenRepository.

Implementation

package com.alcalist.tecmeli.core.network

import com.alcalist.tecmeli.domain.repository.TokenRepository
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject

class AuthInterceptor @Inject constructor(
    private val tokenRepository: TokenRepository
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        
        // Avoid adding token for authentication endpoint
        if (originalRequest.url.encodedPath.contains("oauth/token")) {
            return chain.proceed(originalRequest)
        }

        val token = tokenRepository.getAccessToken()
        val requestBuilder = originalRequest.newBuilder()
        
        token?.let {
            requestBuilder.addHeader("Authorization", "Bearer $it")
        }
        
        return chain.proceed(requestBuilder.build())
    }
}

Parameters

tokenRepository
TokenRepository
required
Repository for retrieving the current access token

Method: intercept()

chain
Interceptor.Chain
The OkHttp interceptor chain
Response
Response
The server response after processing the request with the token
The interceptor skips adding the token for requests to the oauth/token endpoint to prevent infinite loops during token refresh.

AppError

Sealed class hierarchy defining application-level errors, decoupling technical exceptions from business logic and UI.

Error Types

sealed class AppError : Exception() {
    /** Network connectivity error (no internet or connection issues) */
    class Network : AppError()

    /** Connection timeout */
    class Timeout : AppError()

    /** Server error (4xx, 5xx) */
    data class Server(val code: Int, val msg: String) : AppError()

    /** Unclassified or unexpected error */
    data class Unknown(val throwable: Throwable) : AppError()
}

Usage Example

when (val error = result.exceptionOrNull()) {
    is AppError.Network -> showMessage("No internet connection")
    is AppError.Timeout -> showMessage("Request timed out")
    is AppError.Server -> showMessage("Server error: ${error.msg}")
    is AppError.Unknown -> showMessage("Unexpected error occurred")
}

HttpResponseHandler

Defines a contract for managing and validating HTTP responses from Retrofit.
interface HttpResponseHandler {
    fun <T, R> handleResponse(
        response: Response<T>,
        transform: (T) -> R
    ): Result<R>
}

ErrorMapper

Defines a contract for transforming technical exceptions to domain errors, following the Dependency Inversion Principle (DIP).
interface ErrorMapper {
    fun mapException(exception: Exception): AppError
}

NetworkLogger

Interface for logging network events and errors.
interface NetworkLogger {
    fun logServerError(code: Int, message: String)
    fun logUnexpectedError(errorType: String, exception: Exception)
}

Best Practices

Don’t make direct Retrofit calls in repositories. Always wrap them with SafeApiCallExecutor to ensure consistent error handling and logging.
Implement exhaustive when statements to handle all AppError variants and provide appropriate user feedback.
You don’t need to manually handle 401 errors. The TokenAuthenticator handles token refresh automatically.
Never use AuthInterceptor on the authentication client to avoid infinite loops during token refresh.

See Also

  • DI Modules - Hilt configuration for network components
  • Utilities - UiState for managing UI state with network results

Build docs developers (and LLMs) love