fix(simkl): centralize oauth request policy

This commit is contained in:
tapframe 2026-07-22 14:08:43 +05:30
parent c1022e2eb5
commit adcc829a1a
3 changed files with 70 additions and 17 deletions

View file

@ -20,12 +20,18 @@ internal enum class SimklHttpMethod {
DELETE,
}
internal enum class SimklRetryPolicy {
TRANSIENT_FAILURES,
NEVER,
}
internal data class SimklApiRequest(
val method: SimklHttpMethod,
val path: String,
val query: Map<String, String> = emptyMap(),
val body: String = "",
val requiresAuthentication: Boolean = true,
val retryPolicy: SimklRetryPolicy = SimklRetryPolicy.TRANSIENT_FAILURES,
val scrobbleStopConflictIsSuccess: Boolean = false,
)
@ -78,7 +84,11 @@ internal class SimklApiClient(
}
var lastTransportFailure: Throwable? = null
for (attempt in 0..MAX_RETRIES) {
val maxRetries = when (request.retryPolicy) {
SimklRetryPolicy.TRANSIENT_FAILURES -> MAX_RETRIES
SimklRetryPolicy.NEVER -> 0
}
for (attempt in 0..maxRetries) {
awaitRateLimit(request.method)
val response = try {
engine.execute(
@ -94,7 +104,7 @@ internal class SimklApiClient(
throw error
} catch (error: Throwable) {
lastTransportFailure = error
if (attempt == MAX_RETRIES) {
if (attempt == maxRetries) {
throw SimklApiException(
status = null,
errorCode = "transport_failure",
@ -112,12 +122,12 @@ internal class SimklApiClient(
return@withLock response.toApiResponse(isSoftSuccess = true)
}
SimklResponseAction.REAUTHENTICATE -> {
onUnauthorized()
if (request.requiresAuthentication) onUnauthorized()
throw response.toApiException(json)
}
SimklResponseAction.FAIL -> throw response.toApiException(json)
SimklResponseAction.RETRY -> {
if (attempt == MAX_RETRIES) throw response.toApiException(json)
if (attempt == maxRetries) throw response.toApiException(json)
sleep(
retryDelayMs(
attempt = attempt,

View file

@ -1,7 +1,6 @@
package com.nuvio.app.features.simkl
import co.touchlab.kermit.Logger
import com.nuvio.app.features.addons.httpRequestRaw
import com.nuvio.app.features.tracking.TrackingAuthProvider
import com.nuvio.app.features.tracking.TrackingCapability
import com.nuvio.app.features.tracking.TrackingProviderDescriptor
@ -220,11 +219,14 @@ object SimklAuthRepository : TrackingAuthProvider {
redirectUri = SimklConfig.REDIRECT_URI,
)
val response = try {
httpRequestRaw(
method = "POST",
url = "$SIMKL_API_BASE_URL/oauth/token",
headers = simklRequestHeaders(contentTypeJson = true),
body = json.encodeToString(request),
SimklApi.client.execute(
SimklApiRequest(
method = SimklHttpMethod.POST,
path = "/oauth/token",
body = json.encodeToString(request),
requiresAuthentication = false,
retryPolicy = SimklRetryPolicy.NEVER,
),
)
} catch (error: CancellationException) {
throw error
@ -235,13 +237,6 @@ object SimklAuthRepository : TrackingAuthProvider {
publish(isLoading = false, error = SimklAuthError.TOKEN_EXCHANGE_FAILED)
return@withLock
}
if (response.status !in 200..299) {
clearPendingAuthorization()
persistMetadata()
publish(isLoading = false, error = SimklAuthError.TOKEN_EXCHANGE_FAILED)
return@withLock
}
val token = runCatching { json.decodeFromString<SimklTokenResponse>(response.body) }
.getOrNull()
?.takeIf { it.accessToken.isNotBlank() }

View file

@ -68,6 +68,54 @@ class SimklApiClientTest {
assertTrue(request.headers.getValue("User-Agent").contains('/'))
}
@Test
fun `single use unauthenticated posts keep metadata and never retry`() = runBlocking {
val engine = RecordingEngine(response(503), response(200))
val harness = TestHarness(engine)
assertFailsWith<SimklApiException> {
harness.client.execute(
SimklApiRequest(
method = SimklHttpMethod.POST,
path = "/oauth/token",
body = "{}",
requiresAuthentication = false,
retryPolicy = SimklRetryPolicy.NEVER,
),
)
}
val request = engine.requests.single()
assertTrue("client_id=" in request.url)
assertTrue("app-name=" in request.url)
assertTrue("app-version=" in request.url)
assertTrue(request.headers.getValue("User-Agent").contains('/'))
assertFalse("Authorization" in request.headers)
assertTrue(harness.sleeps.isEmpty())
assertFalse(harness.wasUnauthorized)
}
@Test
fun `unauthenticated 401 does not invalidate an existing session`() = runBlocking {
val engine = RecordingEngine(response(401))
val harness = TestHarness(engine)
assertFailsWith<SimklApiException> {
harness.client.execute(
SimklApiRequest(
method = SimklHttpMethod.POST,
path = "/oauth/token",
body = "{}",
requiresAuthentication = false,
retryPolicy = SimklRetryPolicy.NEVER,
),
)
}
assertFalse(harness.wasUnauthorized)
assertEquals(1, engine.requests.size)
}
@Test
fun `authenticated requests are serialized at documented method rates`() = runBlocking {
val engine = RecordingEngine(response(200), response(200), response(200), response(200))