Skip to content

Commit f54cda1

Browse files
committed
okhttp: add retry interceptor
1 parent c13ad75 commit f54cda1

File tree

5 files changed

+151
-51
lines changed

5 files changed

+151
-51
lines changed

app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.thewizrd.shared_resources.exceptions.WeatherException
1818
import com.thewizrd.shared_resources.locationdata.LocationData
1919
import com.thewizrd.shared_resources.locationdata.WeatherLocationProvider
2020
import com.thewizrd.shared_resources.locationdata.toLocationData
21+
import com.thewizrd.shared_resources.okhttp3.OkHttp3Utils.await
2122
import com.thewizrd.shared_resources.preferences.SettingsManager
2223
import com.thewizrd.shared_resources.remoteconfig.WeatherProviderConfig
2324
import com.thewizrd.shared_resources.utils.Coordinate
@@ -50,6 +51,7 @@ import com.thewizrd.weather_api.weatherkit.CurrentWeather
5051
import kotlinx.coroutines.Dispatchers
5152
import kotlinx.coroutines.runBlocking
5253
import kotlinx.coroutines.withContext
54+
import okhttp3.Request
5355
import org.junit.After
5456
import org.junit.Assert.*
5557
import org.junit.Before
@@ -688,4 +690,43 @@ class UnitTests {
688690
UpdateInfo::class.java
689691
)
690692
}
693+
694+
@Test
695+
fun test500error() {
696+
val request = Request.Builder()
697+
.url("https://httpstat.us/500")
698+
.build()
699+
700+
val client = sharedDeps.httpClient
701+
702+
runBlocking {
703+
client.newCall(request).await()
704+
}
705+
}
706+
707+
@Test
708+
fun test502error() {
709+
val request = Request.Builder()
710+
.url("https://httpstat.us/502?sleep=1000")
711+
.build()
712+
713+
val client = sharedDeps.httpClient
714+
715+
runBlocking {
716+
client.newCall(request).await()
717+
}
718+
}
719+
720+
@Test
721+
fun test504error() {
722+
val request = Request.Builder()
723+
.url("https://httpstat.us/504?sleep=1000")
724+
.build()
725+
726+
val client = sharedDeps.httpClient
727+
728+
runBlocking {
729+
client.newCall(request).await()
730+
}
731+
}
691732
}

shared_resources/src/main/java/com/thewizrd/shared_resources/SharedModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.thewizrd.shared_resources
33
import android.content.Context
44
import com.thewizrd.shared_resources.icons.WeatherIconsManager
55
import com.thewizrd.shared_resources.okhttp3.CacheInterceptor
6+
import com.thewizrd.shared_resources.okhttp3.RetryPolicyInterceptor
67
import com.thewizrd.shared_resources.preferences.SettingsManager
78
import com.thewizrd.shared_resources.utils.Logger
89
import okhttp3.Cache
@@ -38,6 +39,7 @@ abstract class SharedModule {
3839
.retryOnConnectionFailure(true)
3940
.cache(Cache(File(context.cacheDir, "okhttp3"), 50L * 1024 * 1024))
4041
.addNetworkInterceptor(CacheInterceptor())
42+
.addInterceptor(RetryPolicyInterceptor())
4143
.build()
4244
}
4345

shared_resources/src/main/java/com/thewizrd/shared_resources/okhttp3/CacheInterceptor.java

Lines changed: 0 additions & 51 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.thewizrd.shared_resources.okhttp3
2+
3+
import okhttp3.CacheControl
4+
import okhttp3.Interceptor
5+
import okhttp3.Response
6+
import java.util.concurrent.TimeUnit
7+
8+
class CacheInterceptor : Interceptor {
9+
companion object {
10+
private const val CACHE_CONTROL_HEADER = "Cache-Control"
11+
private const val CACHE_CONTROL_NO_CACHE = "no-cache"
12+
}
13+
14+
override fun intercept(chain: Interceptor.Chain): Response {
15+
val request = chain.request()
16+
val response = chain.proceed(request)
17+
18+
val shouldUseCache =
19+
!CACHE_CONTROL_NO_CACHE.equals(request.header(CACHE_CONTROL_HEADER), ignoreCase = true)
20+
21+
if (!shouldUseCache) {
22+
return response
23+
}
24+
25+
val hasCacheHeader = !request.header(CACHE_CONTROL_HEADER).isNullOrEmpty()
26+
27+
// Override server cache protocol
28+
val builder = response.newBuilder()
29+
.removeHeader("Pragma")
30+
31+
if (!hasCacheHeader) {
32+
// If original response does not contain a Cache-Control header
33+
// cache the response for a minimum of 2 min to avoid repeat requests
34+
val cacheControl = CacheControl.Builder()
35+
.maxAge(2, TimeUnit.MINUTES)
36+
.build()
37+
38+
builder.header(CACHE_CONTROL_HEADER, cacheControl.toString())
39+
} else {
40+
builder.header(CACHE_CONTROL_HEADER, request.cacheControl.toString())
41+
}
42+
43+
return builder.build()
44+
}
45+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.thewizrd.shared_resources.okhttp3
2+
3+
import com.thewizrd.shared_resources.utils.Logger
4+
import kotlinx.coroutines.delay
5+
import kotlinx.coroutines.runBlocking
6+
import okhttp3.Interceptor
7+
import okhttp3.Response
8+
import okhttp3.internal.closeQuietly
9+
import okio.IOException
10+
import java.net.HttpURLConnection
11+
import kotlin.math.pow
12+
13+
class RetryPolicyInterceptor : Interceptor {
14+
companion object {
15+
private const val TAG = "RetryPolicyInterceptor"
16+
17+
private val RETRYABLE_STATUSES = setOf(
18+
HttpURLConnection.HTTP_INTERNAL_ERROR,
19+
HttpURLConnection.HTTP_BAD_GATEWAY,
20+
//HttpURLConnection.HTTP_UNAVAILABLE, -- RetryAndFollowUpInterceptor
21+
HttpURLConnection.HTTP_GATEWAY_TIMEOUT,
22+
)
23+
24+
private const val DEFAULT_RETRY_COUNT = 2
25+
private const val DEFAULT_RETRY_DELAY_MS = 300
26+
27+
private const val HEADER_RETRY_COUNT = "X-Retry-Count"
28+
private const val HEADER_RETRY_AFTER = "Retry-After"
29+
}
30+
31+
override fun intercept(chain: Interceptor.Chain): Response {
32+
val request = chain.request()
33+
var response = chain.proceed(request)
34+
35+
val retryDelay = (response.header(HEADER_RETRY_AFTER)?.toIntOrNull()?.times(1000)
36+
?: DEFAULT_RETRY_DELAY_MS).coerceAtMost(10000)
37+
val retryCount = (request.header(HEADER_RETRY_COUNT)?.toIntOrNull() ?: DEFAULT_RETRY_COUNT)
38+
39+
var tryCount = 0
40+
41+
while ((!response.isSuccessful && response.code in RETRYABLE_STATUSES) && tryCount < retryCount) {
42+
response.closeQuietly()
43+
44+
val expDelay = retryDelay * 2f.pow(tryCount)
45+
tryCount++
46+
47+
runBlocking {
48+
try {
49+
delay(expDelay.toLong())
50+
response = chain.proceed(request.newBuilder().build())
51+
Logger.debug(
52+
TAG,
53+
"retried request - tryCount = $tryCount | host: ${request.url.host} | statusCode: ${response.code}"
54+
)
55+
} catch (e: IOException) {
56+
Logger.error(TAG, e)
57+
}
58+
}
59+
}
60+
61+
return response
62+
}
63+
}

0 commit comments

Comments
 (0)