Skip to content

Use fallback API hosts when receiving server down response #2368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ internal class AppConfig(
val baseURL: URL = proxyURL?.also {
log(LogIntent.INFO, ConfigureStrings.CONFIGURING_PURCHASES_PROXY_URL_SET)
} ?: URL("https://api.revenuecat.com/")
val fallbackBaseURLs: List<URL> = if (proxyURL != null) {
emptyList()
} else {
listOf(URL("https://api-production.8-lives-cat.io/"))
}
val customEntitlementComputation: Boolean
get() = dangerousSettings.customEntitlementComputation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ internal class Backend(
body = null,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -272,6 +273,7 @@ internal class Backend(
body,
postFieldsToSign,
backendHelper.authenticationHeaders + extraHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -338,6 +340,7 @@ internal class Backend(
body = null,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -402,6 +405,7 @@ internal class Backend(
body,
postFieldsToSign,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -455,6 +459,7 @@ internal class Backend(
body,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -516,6 +521,7 @@ internal class Backend(
body,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -564,6 +570,7 @@ internal class Backend(
body = null,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -617,6 +624,7 @@ internal class Backend(
body = null,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down Expand Up @@ -677,6 +685,7 @@ internal class Backend(
body,
postFieldsToSign = null,
backendHelper.authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class BackendHelper(
body,
postFieldsToSign,
authenticationHeaders,
fallbackBaseURLs = appConfig.fallbackBaseURLs,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ internal class HTTPClient(
* @throws JSONException Thrown for any JSON errors, not thrown for returned HTTP error codes
* @throws IOException Thrown for any unexpected errors, not thrown for returned HTTP error codes
*/
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
@Throws(JSONException::class, IOException::class)
fun performRequest(
baseURL: URL,
Expand All @@ -108,6 +108,8 @@ internal class HTTPClient(
postFieldsToSign: List<Pair<String, String>>?,
requestHeaders: Map<String, String>,
refreshETag: Boolean = false,
fallbackBaseURLs: List<URL> = emptyList(),
fallbackURLIndex: Int = 0,
): HTTPResult {
if (appConfig.forceServerErrors) {
warnLog("Forcing server error for request to ${endpoint.getPath()}")
Expand Down Expand Up @@ -136,7 +138,36 @@ internal class HTTPClient(
}
if (callResult == null) {
log(LogIntent.WARNING, NetworkStrings.ETAG_RETRYING_CALL)
callResult = performRequest(baseURL, endpoint, body, postFieldsToSign, requestHeaders, refreshETag = true)
callResult = performRequest(
baseURL,
endpoint,
body,
postFieldsToSign,
requestHeaders,
refreshETag = true,
fallbackBaseURLs,
fallbackURLIndex,
)
} else if (RCHTTPStatusCodes.isServerError(callResult.responseCode) &&
endpoint.supportsFallbackBaseURLs &&
fallbackURLIndex in fallbackBaseURLs.indices
) {
// Handle server errors with fallback URLs
val fallbackBaseURL = fallbackBaseURLs[fallbackURLIndex]
log(
LogIntent.DEBUG,
NetworkStrings.RETRYING_CALL_WITH_FALLBACK_URL.format(endpoint.getPath(), fallbackBaseURL),
)
callResult = performRequest(
fallbackBaseURL,
endpoint,
body,
postFieldsToSign,
requestHeaders,
refreshETag,
fallbackBaseURLs,
fallbackURLIndex + 1,
)
}
return callResult
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,24 @@ internal sealed class Endpoint(val pathTemplate: String, val name: String) {
->
false
}

val supportsFallbackBaseURLs: Boolean
get() = when (this) {
is GetOfferings,
GetProductEntitlementMapping,
->
true

is LogIn,
PostReceipt,
PostRedeemWebPurchase,
is GetAmazonReceipt,
is PostAttributes,
PostDiagnostics,
PostPaywallEvents,
is GetCustomerInfo,
is GetCustomerCenterConfig,
->
false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal object NetworkStrings {
const val HTTP_RESPONSE_PAYLOAD_NULL = "HTTP Response payload is null"
const val ETAG_RETRYING_CALL = "We were expecting to be able to return a cached response, but we can't find it. " +
"Retrying call with a new ETag"
const val RETRYING_CALL_WITH_FALLBACK_URL = "Retrying request %s using fallback URL %s"
const val ETAG_CALL_ALREADY_RETRIED = "We can't find the cached response, but call has already been retried. " +
"Returning result from backend: %s"
const val SAME_CALL_SCHEDULED_WITHOUT_JITTER = "Request already scheduled without jitter delay, adding " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class AmazonBackendTest {
mockClient = mockk()
mockAppConfig = mockk<AppConfig>().apply {
every { baseURL } returns [email protected]
every { fallbackBaseURLs } returns emptyList()
}
dispatcher = SyncDispatcher()
backendHelper = BackendHelper(API_KEY, dispatcher, mockAppConfig, mockClient)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ internal abstract class BaseBackendIntegrationTest {
every { forceServerErrors } returns false
every { forceSigningErrors } returns false
every { isAppBackgrounded } returns false
every { fallbackBaseURLs } returns emptyList()
}
dispatcher = Dispatcher(Executors.newSingleThreadScheduledExecutor(), runningIntegrationTests = true)
diagnosticsDispatcher = Dispatcher(Executors.newSingleThreadScheduledExecutor(), runningIntegrationTests = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,36 @@ class AppConfigTest {
"showInAppMessagesAutomatically=false, " +
"baseURL=https://api.revenuecat.com/)")
}

// region Fallback API host

@Test
fun `appConfig returns expected fallback URLs list when no proxy URL is set`() {
val appConfig = AppConfig(
context = mockk(relaxed = true),
purchasesAreCompletedBy = REVENUECAT,
showInAppMessagesAutomatically = false,
platformInfo = PlatformInfo(flavor = "native", version = "3.2.0"),
proxyURL = null,
store = Store.PLAY_STORE,
isDebugBuild = false,
)
assertThat(appConfig.fallbackBaseURLs).isEqualTo(listOf(URL("https://api-production.8-lives-cat.io/")))
}

@Test
fun `appConfig returns empty fallback URLs list when a proxy URL is set`() {
val appConfig = AppConfig(
context = mockk(relaxed = true),
purchasesAreCompletedBy = REVENUECAT,
showInAppMessagesAutomatically = false,
platformInfo = PlatformInfo(flavor = "native", version = "3.2.0"),
proxyURL = URL("https://proxy.com"),
store = Store.PLAY_STORE,
isDebugBuild = false,
)
assertThat(appConfig.fallbackBaseURLs).isEmpty()
}

// endregion Fallback API host
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ internal abstract class BaseHTTPClientTest {
endpoint: Endpoint,
expectedResult: HTTPResult,
verificationResult: VerificationResult = VerificationResult.NOT_REQUESTED,
requestDateHeader: Date? = null
requestDateHeader: Date? = null,
server: MockWebServer = this.server,
) {
every {
mockETagManager.getHTTPResultFromCacheOrBackend(
Expand Down
Loading