OkHttp Interceptors

Interceptors in Retrofit's OkHttp Client. When and How to use them.

2023-04-16 07:52:38 - Mohamad Abuzaid

Interceptors are a powerful feature in Retrofit, a popular HTTP client library for Android, that allows you to modify outgoing requests and incoming responses before they are sent and received by the server. They are a chain of functions that can be added to the OkHttp (Retrofit underlying HTTP client)’s pipeline, allowing you to inspect and modify request headers, add or remove parameters, log network traffic, and handle error responses, among other things.

Interceptors are implemented using the Interceptor interface provided by the OkHttp library. This interface has two methods: intercept() and chain(). The intercept() method takes an InterceptorChain object, which represents the current request/response chain. This object provides access to the request being made, the response that will be received, and the next interceptor in the chain, if any. The intercept() method can modify the request or response as needed and then call chain.proceed() to pass the request/response on to the next interceptor or the server.

To use interceptors in Retrofit, you need to create an instance of the OkHttpClient class, which is responsible for handling network requests, and add one or more interceptors to its pipeline using the addInterceptor() method.


val client = OkHttpClient.Builder()
    .addInterceptor(LoggingInterceptor())
    .addInterceptor(/* some other interceptor */)
    .addInterceptor(/* some other interceptor */)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Here, we create an instance of OkHttpClient and add a LoggingInterceptor to its pipeline using the addInterceptor() method. This interceptor logs the outgoing request and the incoming response to the console for debugging purposes. Finally, we pass this OkHttpClient instance to the Retrofit.Builder() method to create a new instance of Retrofit.

-----------------------------


There are many use cases for interceptors in Retrofit, including:

  1. Logging: You can use an interceptor to log network traffic to the console or to a file for debugging purposes. You can log request headers, request and response bodies, response headers, and response codes.
  2. Authentication: You can use an interceptor to add an authentication token to the request headers before it’s sent to the server.
  3. Caching: You can use an interceptor to cache responses from the server, reducing network traffic and improving performance.
  4. Error handling: You can use an interceptor to handle error responses from the server, such as invalid credentials or network timeouts.
  5. Request/Response manipulation: You can use an interceptor to modify the request or response before it’s sent or received by the server. For example, you can add or remove query parameters, change the request method, or modify the response body.

Overall, interceptors allow you to customize the behavior of the HTTP client in a variety of ways, making it easier to integrate with different APIs and services.

---------------------


[1] Logging


Interceptors can be used to log the network traffic for debugging and monitoring purposes. By adding a logging interceptor to the Retrofit client’s pipeline, we can print out information about the requests and responses that are sent and received.


class LoggingInterceptor : Interceptor {
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = Level.BODY // set the logging level
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        // log the request
        val request = chain.request()
        loggingInterceptor.intercept(chain)

        // proceed with the request
        val response = chain.proceed(request)

        // log the response
        loggingInterceptor.intercept(chain)

        return response
    }
}

In this example, we create a LoggingInterceptor class that implements the Interceptor interface. Inside the intercept method, we first create an instance of HttpLoggingInterceptor, which is a pre-built interceptor provided by the OkHttp library. We set the logging level to BODY, which logs the request and response headers and bodies.

Next, we log the request by calling the intercept method on the HttpLoggingInterceptor object with the current chain object. This logs the request headers and body to the console.

We then proceed with the request by calling chain.proceed(request), which sends the request to the server and returns the response.

Finally, we log the response by calling the intercept method on the HttpLoggingInterceptor object again, passing in the same chain object. This logs the response headers and body to the console.

------------------------


[2] Authentication


Interceptors can also be used for authentication, by adding an authorization header to outgoing requests or by intercepting incoming responses to check for authentication errors.


class AuthenticationInterceptor(private val authToken: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val authenticatedRequest = request.newBuilder()
            .header("Authorization", "Bearer $authToken")
            .build()
        return chain.proceed(authenticatedRequest)
    }
}

In this example, we create an AuthenticationInterceptor class that implements the Interceptor interface. We pass in an authToken parameter in the constructor, which is the authentication token that we want to use for all outgoing requests.

Inside the intercept method, we first get the current request object from the chain. We then create a new request object using the newBuilder() method of the original request. We set the “Authorization” header of the new request to the authToken that we passed in, with the “Bearer” prefix.

Finally, we call chain.proceed(authenticatedRequest) to send the authenticated request to the server and get the response.

------------------------


[3] Caching


Interceptors can also be used for caching, by intercepting network requests and responses to store and retrieve data from a local cache. This can help to reduce network traffic and improve performance by serving cached data instead of making new requests to the server.


class CachingInterceptor(private val cacheSize: Long) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val cacheControl = CacheControl.Builder()
            .maxAge(1, TimeUnit.DAYS) // cache for 1 day
            .build()
        val offlineCacheControl = CacheControl.Builder()
            .maxStale(7, TimeUnit.DAYS) // serve stale cache for up to 7 days
            .build()
        val response = chain.proceed(request)
        return response.newBuilder()
            .header("Cache-Control", cacheControl.toString())
            .header("Cache-Control", offlineCacheControl.toString())
            .build()
    }
}

In this example, we create a CachingInterceptor class that implements the Interceptor interface. We pass in a cacheSize parameter in the constructor, which is the size of the cache that we want to use in bytes.

Inside the intercept method, we first get the current request object from the chain. We then create two CacheControl objects: one for online caching, which caches responses for 1 day, and one for offline caching, which serves stale cache for up to 7 days.

We then send the request to the server using chain.proceed(request) and get the response.

Finally, we create a new response object using response.newBuilder(), set the “Cache-Control” header to the online and offline cache control objects using header(), and build and return the new response object using build().

----------------------


[4] Error Handling

A. Authentication Exceptions

Interceptors can be used to handle authentication exceptions that may occur during network requests. For example, if a user’s access token has expired or if they do not have the necessary permissions to access a certain resource, the server may return a 401 or 403 response code. In such cases, we can use an interceptor to automatically refresh the access token or prompt the user to log in again.


class AuthInterceptor(private val authManager: AuthManager) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        var response = chain.proceed(request)

        if (response.code() == 401 || response.code() == 403) {
            // Access token has expired or user does not have necessary permissions
            authManager.refreshToken() // Refresh access token
            val newRequest = request.newBuilder()
                .header("Authorization", "Bearer ${authManager.getAccessToken()}")
                .build()
            response.close()
            response = chain.proceed(newRequest)
        }

        return response
    }
}

In this example, we create an AuthInterceptor class that implements the Interceptor interface. We pass in an authManager object in the constructor, which is responsible for managing the user’s authentication state and access tokens.

Inside the intercept method, we first get the current request object from the chain. We then send the request to the server using chain.proceed(request) and get the response.

If the response code is 401 or 403, we know that the user’s access token has expired or they do not have the necessary permissions to access the resource. In such cases, we call authManager.refreshToken() to refresh the access token. We then create a new request object with the updated access token using request.newBuilder() and set the “Authorization” header to the new access token using header(). We then send the new request to the server using chain.proceed(newRequest) and get the updated response.

Finally, we return the response object.

-----------------------


B. Server Side Exceptions

Interceptors can also be used to handle server-side exceptions that may occur during network requests. For example, if a server-side exception occurs, the server may return a 5xx response code. In such cases, we can use an interceptor to intercept the response and handle the error gracefully.


class ErrorInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val response = chain.proceed(request)

        if (!response.isSuccessful) {
            // Server-side error occurred
            throw ServerException(response.code(), response.message())
        }

        return response
    }
}

class ServerException(val errorCode: Int,
    message: String?) : RuntimeException(message)

In this example, we create an ErrorInterceptor class that implements the Interceptor interface. Inside the intercept method, we get the current request object from the chain and send the request to the server using chain.proceed(request). We then get the response object and check if it was successful using response.isSuccessful.

If the response is not successful, we throw a ServerException with the error code and message from the response. We can then catch this exception and handle it gracefully in our application code.

In the ServerException class, we extend RuntimeException and store the error code and message in properties. We can then use these properties to display a user-friendly error message or log the error for debugging purposes.

------------------------


C. Data Exceptions

Interceptors can also be used to handle data exceptions that may occur during network requests. For example, if the response data from the server does not match the expected format, we may want to intercept the response and handle the error gracefully.


class DataInterceptor(private val gson: Gson) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val response = chain.proceed(request)

        val contentType = response.body()?.contentType()
        val bodyString = response.body()?.string()

        val modifiedResponse = response.newBuilder()
            .body(bodyString?.toResponseBody(contentType))
            .build()

        try {
            // Parse the response body and throw an exception if it is not valid
            val data = gson.fromJson(bodyString, Data::class.java)
            if (!data.isValid()) {
                throw DataException(data.errorMessage)
            }
        } catch (e: Exception) {
            // If there was an error parsing the response body, throw a DataException
            throw DataException("Invalid response data")
        }

        return modifiedResponse
    }
}

class DataException(message: String?) : RuntimeException(message)

data class Data(val errorMessage: String) {
    fun isValid() = errorMessage.isBlank()
}

In this example, we create a DataInterceptor class that implements the Interceptor interface. Inside the intercept method, we get the current request object from the chain and send the request to the server using chain.proceed(request). We then get the response object and parse the response body using a Gson instance.

If there is an error parsing the response body or if the data is not valid, we throw a DataException with an error message. We can then catch this exception and handle it gracefully in our application code.

In the DataException class, we extend RuntimeException and store the error message in a property. We can then use this property to display a user-friendly error message or log the error for debugging purposes.

-----------------------------


[5] Request/Response Manipulation


Interceptors can be used to manipulate the requests and responses that are sent and received from the server. For example, we can modify the headers or body of a request, or add custom headers to the response.


class RequestResponseInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // Get the current request from the chain
        val request = chain.request()

        // Modify the request headers
        val modifiedRequest = request.newBuilder()
            .addHeader("User-Agent", "MyApp/1.0")
            .build()

        // Send the modified request to the server and get the response
        val response = chain.proceed(modifiedRequest)

        // Modify the response headers
        val modifiedResponse = response.newBuilder()
            .addHeader("X-App-Version", "1.0")
            .build()

        return modifiedResponse
    }
}

In this example, we create a RequestResponseInterceptor class that implements the Interceptor interface. Inside the intercept method, we get the current request object from the chain and modify the request headers using the addHeader() method. We then send the modified request to the server using chain.proceed(modifiedRequest) and get the response object.

We can then modify the response headers using the addHeader() method again and return the modified response object.


More Posts