A deep dive into OkHttp interceptors
OkHttp stands as a widely recognized library in both Android and Java development. Many HTTP libraries within the Android ecosystem, such as Retrofit, Glide, Coil, and more, leverage OkHttp as their underlying engine. Today, we’ll delve into the realm of OkHttp Interceptors, discovering how they can significantly enhance our development processes.
Table of contents:
- Overview of OkHttp Interceptors
- Types of Interceptors
- Chaining Interceptors
- Logging Interceptor
- Integrating OkHttp Interceptors with other libraries
Overview of OkHttp Interceptors:
Before we learn about Interceptors let’s first take a real-world example of a situation where we would need to implement Interceptors in our application code.
As shown in Image 1, we have an android application that makes http requests to a server and gets the http responses from the later. In our Android app, we use OkHttp to send HTTP requests to a server and receive responses. Initially, our code looks straightforward:
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// Handle response failure
}
override fun onResponse(call: Call, response: okhttp3.Response) {
// Handle response success
}
})
Please note that for the sake of simplicity, we’ve omitted the use of dependency injection in this example. In a real-world project, it’s highly recommended to employ dependency injection for improved code maintainability.
However, when dealing with a secure server that requires an authentication token, we modify the code to include the token in the request header:
val request = Request.Builder()
.header("Authorization", "Bearer ${token}")
.url(url)
.build()
While this works, it becomes cumbersome in a real-world application with multiple server requests. To streamline this process and avoid adding the authentication header to each network call, we can use an OkHttp interceptor. This interceptor intercepts each network call, allowing us to modify the request centrally.
Here’s how we can achieve this:
val authInterceptor = Interceptor { chain ->
// Intercept the request
val originalRequest = chain.request()
// Add the Authorization header with the token
val requestWithToken = originalRequest.newBuilder()
.header("Authorization", "Bearer ${token}")
.build()
// Proceed with the modified request
chain.proceed(requestWithToken)
}
val client = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
- To modify the current outgoing request, we utilize
chain.request()
, which provides access to the request being sent. - The
newBuilder()
method is then invoked on the request, allowing us to create a mutable copy of it. This mutable copy can be modified without altering the original request. - The final and critical step involves passing the modified request back to OkHttp to be sent. We accomplish this by using
chain.proceed(requestWithToken)
. chain.proceed()
not only sends the modified request but also returns the response obtained from the server.- We utilize the
addInterceptor()
method onOkHttpClient.Builder()
to seamlessly integrate anInterceptor
into each network call executed by ourOkHttpClient
. In typical practice, theOkHttpClient
is often implemented as a singleton—a single, globally accessible instance configured once and leveraged consistently across the entire application. This ensures uniform application of the interceptor logic, enhancing the efficiency and maintainability of our network communication setup.
We’ve explored the interception process for HTTP requests, and the procedure for handling responses aligns seamlessly. As mentioned previously, chain.proceed()
yields the response, allowing us to conveniently customize it before forwarding the modified version to our application code. In this context, a response interceptor is implemented as follows:
val responseInterceptor = Interceptor { chain ->
// Intercept the response
val response = chain.proceed(chain.request())
// Modify the response by adding a custom header
response.header("Modify Headers","Anything")
// Dispatch the modified response to the application code
response
}
Here, the interceptor captures the response, applies modifications — such as adding a custom header — and then returns the tailored response to be seamlessly integrated into our application’s processing flow.
Types of Interceptors:
Within OkHttp, there exist two distinctive types of interceptors: Application Interceptors
and Network Interceptors
. In our example, the interceptor employed is an Application Interceptor.
In the realm of OkHttp, Application Interceptors and Network Interceptors may seem alike at first glance. For Application Interceptors, we employ addInterceptor()
on OkHttpClient.Builder()
, while for Network Interceptors, we use addNetworkInterceptor()
. However, their functionality diverges behind the scenes.
In a nutshell, Application Interceptors operate before our request reaches OkHttp core code during a request scenario. The response then traverses OkHttp core code before reaching our application code. Conversely, Network Interceptors serve as the final layer before a request heads to the server and the initial layer when we receive a response.
Notably, when caching HTTP responses within OkHttp core, Network Interceptors may not be invoked if the response is retrieved from the cache. In contrast, Application Interceptors are consistently invoked, regardless of caching, ensuring their involvement in every scenario.
When our server redirects a request to a different endpoint, the Network Interceptors step in to capture this event, equipped with the knowledge of redirects and retries. On the flip side, Application Interceptors focus solely on the primary request and the ultimate response, neglecting intricacies related to redirects and retries.
The differences from the official documentation:
Application interceptors
- Don’t need to worry about intermediate responses like redirects and retries.
- Are always invoked once, even if the HTTP response is served from the cache.
- Observe the application’s original intent. Unconcerned with OkHttp-injected headers like
If-None-Match
. - Permitted to short-circuit and not call
Chain.proceed()
.
This means an application interceptor can decide to take a shortcut and not let the request continue its usual journey. It’s like saying, “Hey, I’ve got this covered, no need to go through the usual steps.”
- Permitted to retry and make multiple calls to
Chain.proceed()
.
This is like saying the interceptor can give the request a few more tries or attempts before deciding what to do. It’s allowed to say, “Let me try again,” and make multiple attempts before making a final decision on how to handle the request.
- Can adjust Call timeouts using withConnectTimeout, withReadTimeout, withWriteTimeout.
Network Interceptors
- Able to operate on intermediate responses like redirects and retries.
- Not invoked for cached responses that short-circuit the network.
- Observe the data just as it will be transmitted over the network.
- Access to the
Connection
that carries the request.
Chaining Interceptors in OkHttp:
Chaining interceptors in OkHttp involves linking multiple interceptors together in a specific order. This chaining mechanism allows each interceptor to perform a specific task or modification in sequence, creating a pipeline of functionality for outgoing requests and incoming responses.
Suppose we have three interceptors: LoggerInterceptor
, AuthInterceptor
, and CacheInterceptor
. Each interceptor contributes a unique functionality to the request-response flow.
// Create instances of interceptors
val loggerInterceptor = LoggerInterceptor()
val authInterceptor = AuthInterceptor()
val cacheInterceptor = CacheInterceptor()
// Build the OkHttp client and chain the interceptors
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggerInterceptor)
.addInterceptor(authInterceptor)
.addInterceptor(cacheInterceptor)
.build()
The order in which interceptors are added to the OkHttp client matters. Requests and responses traverse the interceptors in the same order they were added. In the example above, the sequence of interceptors is LoggerInterceptor
→ AuthInterceptor
→ CacheInterceptor
.
When an HTTP request is made, it traverses the interceptors in the order they were added. Each interceptor has the opportunity to modify the request before passing it to the next one.
// LoggerInterceptor logs the request details
// AuthInterceptor adds authentication headers
// CacheInterceptor performs caching-related operations
Similarly, when the response is received, it travels through the interceptors in the reverse order. Each interceptor can modify the response before passing it back up the chain.
// CacheInterceptor processes the response for caching
// AuthInterceptor inspects and potentially modifies the response
// LoggerInterceptor logs the response details
The order of interceptors is crucial because modifications made by one interceptor can influence the subsequent interceptors in the chain. Carefully consider the sequence based on your requirements; for instance, authentication headers might need to be added before logging or caching operations.
Logging Interceptor:
Square has already a logging interceptor built for OkHttp. By integrating this logging interceptor, you can effortlessly generate logs that provide insights into the details of your network operations.
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY // Adjust the logging level as needed
}
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor) // Add the logging interceptor to the OkHttpClient
.build()
// Use the client for network requests as usual
val request = Request.Builder()
.url("https://example.com/api/data")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// Handle response failure
}
override fun onResponse(call: Call, response: okhttp3.Response) {
// Handle response success
}
})
OkHttp logging interceptor, HttpLoggingInterceptor
, offers various customization options to tailor the logging output according to your preferences. Here are additional examples showcasing different aspects of customization:
- Customizing Log Level:
You can customize the logging level to control the amount of information logged. Here are the available levels:
NONE
: No logging.BASIC
: Logs request and response lines.HEADERS
: Logs request and response lines and their headers if present.BODY
: Logs request and response lines and their respective headers and bodies.
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS // Customize the logging level
}
- Customizing Log Format:
If you want to change the format of the logged output, you can provide a custom logger to the interceptor. Here’s an example using a custom logger that logs messages in a different format:
val customLogger = object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
// Customize the log format
println("Custom Logger: $message")
}
}
val loggingInterceptor = HttpLoggingInterceptor(customLogger)
- Writing Logs to a File:
You may want to direct the logs to a file rather than the console. This can be achieved by using a File
as the logger:
val logFile = File("/path/to/log/file.txt")
val fileLogger = object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
logFile.appendText("$message\n")
}
}
val loggingInterceptor = HttpLoggingInterceptor(fileLogger)
- Conditional Logging:
You can conditionally enable or disable logging based on certain criteria. For example, logging only for specific URLs:
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
redactHeader("Authorization") // Redact sensitive information
addConditionalInterceptor { request -> request.url.toString().contains("api") }
}
In this example, the interceptor is set to log at the BODY
level but conditionally only for requests where the URL contains "api".
The redactHeader
method in OkHttp's HttpLoggingInterceptor
is used to redact (mask or hide) sensitive information in the logs. This is particularly useful when logging includes headers that may contain sensitive or confidential information, such as an authorization token.
Integrating OkHttp Interceptors with other libraries:
OkHttp Interceptors extend their utility beyond the confines of OkHttp itself; they seamlessly integrate with various libraries within the Android ecosystem. This versatility allows us to enhance network requests in conjunction with other popular libraries. Let’s explore how to employ OkHttp Interceptors with some well-known Android libraries:
- Retrofit:
Retrofit, developed by the same team behind OkHttp, is a powerful HTTP client for Android and Java applications. Since Retrofit is built on top of OkHttp, integrating OkHttp Interceptors into Retrofit’s network calls is straightforward. This allows us to inject additional functionality seamlessly.
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(MyCustomInterceptor())
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.build()
- Coil
Coil, an image loading library for Android backed by Kotlin Coroutines, aligns its interceptor design with OkHttp Interceptors. This provides a consistent approach to intercepting image loading operations and applying custom behaviors.
class AuthInterceptor() : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val modifiedRequest = chain.request.newBuilder()
.addHeader("Authorization", "Bearer $authToken")
.build()
return chain.proceed(modifiedRequest)
}
}
val imageLoader = ImageLoader.Builder(context)
.components {
add(AuthInterceptor())
}
.build()
- ExoPlayer (Media3)
ExoPlayer leverages the OkHttp library as its network stack for underlying network requests. Instead of modifying the ExoPlayer code directly to add headers or customize behavior, it is recommended to utilize the already configured OkHttpClient. This ensures a unified network stack for all networking activities within your app.
implementation 'androidx.media3:media3-datasource-okhttp:1.X.X'
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(MyCustomInterceptor())
.build()
val okHttpDataSource = OkHttpDataSource.Factory(okHttpClient)
val dataSourceFactory = DefaultDataSource.Factory(context, okHttpDataSource)
ExoPlayer.Builder(context)
.setMediaSourceFactory(
DefaultMediaSourceFactory(context).setDataSourceFactory(dataSource)
)
.build()
And that wraps it up! If you’re keen on exploring OkHttp Interceptors in a real-world application, feel free to dive into the source code of my Android app, Cloud-Castle. For more insights into Android development, you can follow me here or catch up with me on Twitter. Thanks for reading, and until next time!