Last active
October 12, 2022 23:39
-
-
Save nwellis/6073d4915ba19f9b1037b442077f2241 to your computer and use it in GitHub Desktop.
OkHttp auth header interceptor with retry logic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import androidx.annotation.WorkerThread | |
| import kotlinx.coroutines.runBlocking | |
| import okhttp3.Interceptor | |
| import okhttp3.Request | |
| import okhttp3.Response | |
| import timber.log.Timber | |
| object Retried | |
| /** | |
| * This will block against infinite retries if multiple interceptors of this type are used. This | |
| * probably isn't necessary however. | |
| */ | |
| fun Request.hasBeenRetried() = tag(Retried::class.java) != null | |
| fun Request.markAsRetry() = newBuilder().tag(Retried::class.java, Retried).build() | |
| /** | |
| * Base implementation for an authorization interceptor which does the following: | |
| * - For every request it will call [getToken] and decorate the request with an auth header | |
| * - If [getToken] returns `null`, then it will skip authorization | |
| * - If [getToken] returns a token, but the HTTP response code is in [notAuthorizedHttpCodes], | |
| * then this will call [refreshToken] and if given a token will attempt to retry the call once. | |
| */ | |
| abstract class AuthHeaderInterceptor : Interceptor { | |
| open val notAuthorizedHttpCodes: Set<Int> = setOf(401) | |
| open val authHeaderName = "Authorization" | |
| open val authHeaderPattern: (token: String) -> String = { "Bearer $it" } | |
| @WorkerThread | |
| abstract fun getToken(): String? | |
| @WorkerThread | |
| abstract suspend fun refreshToken(): String? | |
| override fun intercept(chain: Interceptor.Chain): Response { | |
| val request = chain.request() | |
| if (request.hasBeenRetried()) return chain.proceed(request) | |
| val token = safely { getToken() } ?: return chain.proceed(request) | |
| val authorizedRequest = request.signWithToken(token) | |
| val response = chain.proceed(authorizedRequest) | |
| return if (notAuthorizedHttpCodes.contains(response.code)) { | |
| safely { runBlocking { refreshToken() } }?.let { newToken -> | |
| response.close() | |
| chain.proceed(request.signWithToken(newToken).markAsRetry()) | |
| } ?: response | |
| } else response | |
| } | |
| private fun Request.signWithToken(token: String): Request { | |
| return newBuilder() | |
| .addHeader(authHeaderName, authHeaderPattern(token)) | |
| .build() | |
| } | |
| private fun <T> safely(block: () -> T): T? = runCatching(block) | |
| .onFailure { | |
| Timber.e(it, "Exception thrown in $this, caught but should not happen.") | |
| }.getOrNull() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment