Skip to content

Commit 0a8684b

Browse files
committed
delay OneSignal network requests if Retry-After
When we get a Retry-After we delay all future requests until after this time as passed. To be safe we assume the server's Retry-After value means all traffic should be delayed until then. If we wanted finer grain control we could work with the the backend to decide which endpoints should be affected.
1 parent 08e5e83 commit 0a8684b

File tree

2 files changed

+56
-1
lines changed

2 files changed

+56
-1
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import com.onesignal.core.internal.http.IHttpClient
1111
import com.onesignal.core.internal.preferences.IPreferencesService
1212
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
1313
import com.onesignal.core.internal.preferences.PreferenceStores
14+
import com.onesignal.core.internal.time.ITime
1415
import com.onesignal.debug.internal.logging.Logging
1516
import kotlinx.coroutines.DelicateCoroutinesApi
1617
import kotlinx.coroutines.Dispatchers
1718
import kotlinx.coroutines.GlobalScope
1819
import kotlinx.coroutines.TimeoutCancellationException
20+
import kotlinx.coroutines.delay
1921
import kotlinx.coroutines.launch
2022
import kotlinx.coroutines.withTimeout
2123
import org.json.JSONObject
@@ -29,7 +31,14 @@ internal class HttpClient(
2931
private val _connectionFactory: IHttpConnectionFactory,
3032
private val _prefs: IPreferencesService,
3133
private val _configModelStore: ConfigModelStore,
34+
private val _time: ITime,
3235
) : IHttpClient {
36+
/**
37+
* Delay making network requests until we reach this time.
38+
* Used when the OneSignal backend returns a Retry-After value.
39+
*/
40+
private var delayNewRequestsUntil = 0L
41+
3342
override suspend fun post(
3443
url: String,
3544
body: JSONObject,
@@ -77,6 +86,9 @@ internal class HttpClient(
7786
return HttpResponse(0, null, null)
7887
}
7988

89+
val delayUntil = delayNewRequestsUntil - _time.currentTimeMillis
90+
if (delayUntil > 0) delay(delayUntil)
91+
8092
try {
8193
return withTimeout(getThreadTimeout(timeout).toLong()) {
8294
return@withTimeout makeRequestIODispatcher(url, method, jsonBody, timeout, cacheKey)
@@ -172,6 +184,8 @@ internal class HttpClient(
172184
httpResponse = con.responseCode
173185

174186
val retryAfter = retryAfterFromResponse(con)
187+
val newDelayUntil = _time.currentTimeMillis + (retryAfter ?: 0) * 1_000
188+
if (newDelayUntil > delayNewRequestsUntil) delayNewRequestsUntil = newDelayUntil
175189

176190
when (httpResponse) {
177191
HttpURLConnection.HTTP_NOT_MODIFIED -> {

OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/http/HttpClientTests.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.onesignal.core.internal.http
22

33
import com.onesignal.common.OneSignalUtils
44
import com.onesignal.core.internal.http.impl.HttpClient
5+
import com.onesignal.core.internal.time.impl.Time
56
import com.onesignal.debug.LogLevel
67
import com.onesignal.debug.internal.logging.Logging
78
import com.onesignal.mocks.MockHelper
@@ -12,14 +13,15 @@ import io.kotest.matchers.shouldBe
1213
import io.kotest.matchers.shouldNotBe
1314
import io.kotest.matchers.types.beInstanceOf
1415
import kotlinx.coroutines.TimeoutCancellationException
16+
import kotlinx.coroutines.withTimeoutOrNull
1517
import org.json.JSONObject
1618

1719
class Mocks {
1820
internal val mockConfigModel = MockHelper.configModelStore()
1921
internal val response = MockHttpConnectionFactory.MockResponse()
2022
internal val factory = MockHttpConnectionFactory(response)
2123
internal val httpClient by lazy {
22-
HttpClient(factory, MockPreferencesService(), mockConfigModel)
24+
HttpClient(factory, MockPreferencesService(), mockConfigModel, Time())
2325
}
2426
}
2527

@@ -208,4 +210,43 @@ class HttpClientTests : FunSpec({
208210
// Then
209211
response.retryAfterSeconds shouldBe 60
210212
}
213+
214+
// If the OneSignal server ever responses with a Retry-After we want to
215+
// make sure we delay any future requests until that delay has past. To
216+
// be safe we assume the server's Retry-After value means all traffic
217+
// should be delayed until then. If we wanted finer grain control we
218+
// could work with the the backend to decide which endpoints should be
219+
// affected.
220+
test("ensure next request is delayed by the Retry-After value") {
221+
// Given
222+
val mocks = Mocks()
223+
224+
val mockRetryAfterResponse = MockHttpConnectionFactory.MockResponse()
225+
mockRetryAfterResponse.status = 429
226+
mockRetryAfterResponse.mockProps["Retry-After"] = "1"
227+
mockRetryAfterResponse.errorResponseBody = "{}"
228+
229+
val mockSuccessfulResponse = MockHttpConnectionFactory.MockResponse()
230+
mockSuccessfulResponse.status = 200
231+
mockSuccessfulResponse.responseBody = "{}"
232+
233+
// When
234+
mocks.factory.mockResponse = mockRetryAfterResponse
235+
mocks.httpClient.post("URL", JSONObject())
236+
237+
mocks.factory.mockResponse = mockSuccessfulResponse
238+
val response2 =
239+
withTimeoutOrNull(999) {
240+
mocks.httpClient.post("URL", JSONObject())
241+
}
242+
243+
val response3 =
244+
withTimeoutOrNull(100) {
245+
mocks.httpClient.post("URL", JSONObject())
246+
}
247+
248+
// Then
249+
response2 shouldBe null
250+
response3 shouldNotBe null
251+
}
211252
})

0 commit comments

Comments
 (0)