Refactor handling of proton error data in json response
- use deserializer from kotlin-utils - merge Proton and Http ApiResult errors
This commit is contained in:
parent
936c4213ab
commit
d14e896863
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Proton Technologies AG
|
||||
* This file is part of Proton Technologies AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package me.proton.core.network.data
|
||||
|
||||
import kotlinx.serialization.SerializationException
|
||||
import me.proton.core.network.domain.ApiResult
|
||||
import me.proton.core.network.domain.NetworkManager
|
||||
import okhttp3.Response
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.security.cert.CertificateException
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
internal suspend fun <Api, T> safeApiCall(
|
||||
networkManager: NetworkManager,
|
||||
api: Api,
|
||||
block: suspend (Api) -> T
|
||||
): ApiResult<T> =
|
||||
try {
|
||||
ApiResult.Success(block(api))
|
||||
} catch (e: ProtonErrorException) {
|
||||
parseHttpError(e.response, e.protonData)
|
||||
} catch (e: HttpException) {
|
||||
parseHttpError(e.response()!!.raw(), null)
|
||||
} catch (e: SerializationException) {
|
||||
ApiResult.Error.Parse
|
||||
} catch (e: CertificateException) {
|
||||
ApiResult.Error.Certificate
|
||||
} catch (e: SSLHandshakeException) {
|
||||
ApiResult.Error.Certificate
|
||||
} catch (e: SocketTimeoutException) {
|
||||
ApiResult.Error.Timeout(networkManager.isConnectedToNetwork())
|
||||
} catch (e: IOException) {
|
||||
ApiResult.Error.Connection(networkManager.isConnectedToNetwork())
|
||||
}
|
||||
|
||||
private fun <T> parseHttpError(response: Response, protonData: ApiResult.Error.ProtonData?): ApiResult<T> {
|
||||
val retryAfter = response.headers["Retry-After"]?.toIntOrNull()
|
||||
return if (response.code == ApiResult.HTTP_TOO_MANY_REQUESTS && retryAfter != null) {
|
||||
ApiResult.Error.TooManyRequest(retryAfter, protonData)
|
||||
} else {
|
||||
ApiResult.Error.Http(response.code, response.message, protonData)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ProtonErrorException(
|
||||
val response: Response,
|
||||
val protonData: ApiResult.Error.ProtonData
|
||||
) : IOException()
|
|
@ -17,9 +17,8 @@
|
|||
*/
|
||||
package me.proton.core.network.data
|
||||
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import me.proton.core.network.data.protonApi.BaseRetrofitApi
|
||||
import me.proton.core.network.data.protonApi.ProtonErrorData
|
||||
import me.proton.core.network.data.protonApi.RefreshTokenRequest
|
||||
import me.proton.core.network.domain.ApiBackend
|
||||
import me.proton.core.network.domain.ApiClient
|
||||
|
@ -28,19 +27,15 @@ import me.proton.core.network.domain.ApiResult
|
|||
import me.proton.core.network.domain.NetworkManager
|
||||
import me.proton.core.network.domain.TimeoutOverride
|
||||
import me.proton.core.network.domain.UserData
|
||||
import me.proton.core.util.kotlin.deserializeOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import retrofit2.Converter
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Retrofit
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.security.cert.CertificateException
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
|
@ -51,7 +46,6 @@ import kotlin.reflect.KClass
|
|||
* @property client [ApiClient] to be used with the backend.
|
||||
* @property userData [UserData] bound to this backend.
|
||||
* @property networkManager [NetworkManager] instance.
|
||||
* @param json kotlin Json parser.
|
||||
* @param converters Retrofit converters to be used in the backend.
|
||||
* @param interfaceClass Kotlin class for [Api].
|
||||
* @param securityStrategy Strategy function introducing to okhttp builder pinning and other
|
||||
|
@ -62,15 +56,12 @@ internal class ProtonApiBackend<Api : BaseRetrofitApi>(
|
|||
private val client: ApiClient,
|
||||
private val userData: UserData,
|
||||
baseOkHttpClient: OkHttpClient,
|
||||
private val json: Json,
|
||||
converters: List<Converter.Factory>,
|
||||
interfaceClass: KClass<Api>,
|
||||
private val networkManager: NetworkManager,
|
||||
securityStrategy: (OkHttpClient.Builder) -> Unit
|
||||
) : ApiBackend<Api> {
|
||||
|
||||
private class ProtonErrorException(val httpCode: Int, val protonCode: Int, val error: String) : IOException()
|
||||
|
||||
private val api: Api
|
||||
|
||||
init {
|
||||
|
@ -126,21 +117,15 @@ internal class ProtonApiBackend<Api : BaseRetrofitApi>(
|
|||
return request
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun interceptErrors(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
// TODO: use serialization functions from kotlin-utils
|
||||
val body = response.peekBody(Long.MAX_VALUE).string()
|
||||
try {
|
||||
val bodyJson = json.parseJson(body)
|
||||
val code = bodyJson.jsonObject["Code"]?.primitive?.int
|
||||
val error = bodyJson.jsonObject["Error"]?.primitive?.content
|
||||
if (code != null && error != null)
|
||||
throw ProtonErrorException(response.code, code, error)
|
||||
} catch (e: SerializationException) {
|
||||
// ignore: not a json response
|
||||
if (!response.isSuccessful) {
|
||||
val errorBody = response.peekBody(MAX_ERROR_BYTES).string()
|
||||
val protonError = errorBody.deserializeOrNull(ProtonErrorData.serializer())?.apiResultData
|
||||
if (protonError != null)
|
||||
throw ProtonErrorException(response, protonError)
|
||||
}
|
||||
|
||||
return response
|
||||
|
@ -149,28 +134,8 @@ internal class ProtonApiBackend<Api : BaseRetrofitApi>(
|
|||
override suspend fun <T> invoke(call: ApiManager.Call<Api, T>): ApiResult<T> =
|
||||
invokeInternal(call.block)
|
||||
|
||||
private suspend fun <T> invokeInternal(block: suspend Api.() -> T): ApiResult<T> = try {
|
||||
ApiResult.Success(block(api))
|
||||
} catch (e: HttpException) {
|
||||
val retryAfter = e.response()?.headers()?.get("Retry-After")?.toIntOrNull()
|
||||
if (e.code() == ApiResult.HTTP_TOO_MANY_REQUESTS && retryAfter != null) {
|
||||
ApiResult.Error.TooManyRequest(retryAfter)
|
||||
} else {
|
||||
ApiResult.Error.Http(e.code(), e.message())
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
ApiResult.Error.Parse
|
||||
} catch (e: CertificateException) {
|
||||
ApiResult.Error.Certificate
|
||||
} catch (e: SSLHandshakeException) {
|
||||
ApiResult.Error.Certificate
|
||||
} catch (e: ProtonErrorException) {
|
||||
ApiResult.Error.Proton(e.httpCode, e.protonCode, e.error)
|
||||
} catch (e: SocketTimeoutException) {
|
||||
ApiResult.Error.Timeout(networkManager.isConnectedToNetwork())
|
||||
} catch (e: IOException) {
|
||||
ApiResult.Error.Connection(networkManager.isConnectedToNetwork())
|
||||
}
|
||||
private suspend fun <T> invokeInternal(block: suspend Api.() -> T): ApiResult<T> =
|
||||
safeApiCall(networkManager, api, block)
|
||||
|
||||
override suspend fun refreshTokens(): ApiResult<ApiBackend.Tokens> {
|
||||
val result = invokeInternal {
|
||||
|
@ -184,4 +149,8 @@ internal class ProtonApiBackend<Api : BaseRetrofitApi>(
|
|||
is ApiResult.Error -> result
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ERROR_BYTES = 1_000_000L
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,6 @@ class ApiFactory(private val apiClient: ApiClient) {
|
|||
apiClient,
|
||||
userData,
|
||||
baseOkHttpClient,
|
||||
ProtonCoreConfig.defaultJsonStringFormat,
|
||||
listOf(jsonConverter),
|
||||
interfaceClass,
|
||||
networkManager,
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
*/
|
||||
package me.proton.core.network.data.protonApi
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.proton.core.network.domain.ApiResult
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
@ -29,3 +32,11 @@ interface BaseRetrofitApi {
|
|||
@GET("test/ping")
|
||||
suspend fun ping()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ProtonErrorData(
|
||||
@SerialName("Code") val code: Int,
|
||||
@SerialName("Error") val error: String
|
||||
) {
|
||||
val apiResultData get() = ApiResult.Error.ProtonData(code, error)
|
||||
}
|
||||
|
|
|
@ -218,7 +218,8 @@ internal class ApiManagerTests {
|
|||
@Test
|
||||
fun `test force update`() = runBlockingTest {
|
||||
coEvery { backend.invoke<TestResult>(any()) } returns
|
||||
ApiResult.Error.Proton(400, ProtonForceUpdateHandler.ERROR_CODE_FORCE_UPDATE, "")
|
||||
ApiResult.Error.Http(400, "",
|
||||
ApiResult.Error.ProtonData(ProtonForceUpdateHandler.ERROR_CODE_FORCE_UPDATE, ""))
|
||||
val result = apiManager.invoke { test() }
|
||||
assertTrue(result is ApiResult.Error)
|
||||
assertEquals(true, apiClient.forceUpdated)
|
||||
|
|
|
@ -30,7 +30,6 @@ import me.proton.core.network.data.util.prepareResponse
|
|||
import me.proton.core.network.domain.ApiManager
|
||||
import me.proton.core.network.domain.ApiResult
|
||||
import me.proton.core.network.domain.NetworkManager
|
||||
import me.proton.core.util.kotlin.ProtonCoreConfig
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
|
@ -56,8 +55,7 @@ internal class ProtonApiBackendTests {
|
|||
fun before() {
|
||||
MockKAnnotations.init(this)
|
||||
val client = MockApiClient()
|
||||
val apiFactory = ApiFactory(client)
|
||||
|
||||
apiFactory = ApiFactory(client)
|
||||
val user = MockUserData()
|
||||
|
||||
every { networkManager.isConnectedToNetwork() } returns isNetworkAvailable
|
||||
|
@ -69,7 +67,6 @@ internal class ProtonApiBackendTests {
|
|||
client,
|
||||
user,
|
||||
apiFactory.baseOkHttpClient,
|
||||
ProtonCoreConfig.defaultJsonStringFormat,
|
||||
listOf(
|
||||
ScalarsConverterFactory.create(),
|
||||
apiFactory.jsonConverter
|
||||
|
@ -128,11 +125,11 @@ internal class ProtonApiBackendTests {
|
|||
"""{ "Code": 10, "Error": "darn!" }""")
|
||||
|
||||
val result = backend(ApiManager.Call(0) { test() })
|
||||
assertTrue(result is ApiResult.Error.Proton)
|
||||
assertTrue(result is ApiResult.Error.Http)
|
||||
|
||||
assertEquals(10, result.protonCode)
|
||||
assertEquals(10, result.proton?.code)
|
||||
assertEquals(401, result.httpCode)
|
||||
assertEquals("darn!", result.error)
|
||||
assertEquals("darn!", result.proton?.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -47,17 +47,10 @@ sealed class ApiResult<out T> {
|
|||
*
|
||||
* @property httpCode HTTP code.
|
||||
* @property message HTTP message.
|
||||
* @property proton Proton-specific HTTP error data.
|
||||
*/
|
||||
open class Http(val httpCode: Int, val message: String) : Error()
|
||||
|
||||
/**
|
||||
* Response with Proton error.
|
||||
*
|
||||
* @property httpCode Response HTTP code.
|
||||
* @property protonCode Response Proton code.
|
||||
* @property error Error string.
|
||||
*/
|
||||
class Proton(httpCode: Int, val protonCode: Int, val error: String) : Http(httpCode, error)
|
||||
open class Http(val httpCode: Int, val message: String, val proton: ProtonData? = null) : Error()
|
||||
class ProtonData(val code: Int, val error: String)
|
||||
|
||||
/**
|
||||
* Parsing error. Should not normally happen.
|
||||
|
@ -96,7 +89,8 @@ sealed class ApiResult<out T> {
|
|||
* @property retryAfterSeconds Number of seconds to hold all requests (network layer will
|
||||
* automatically fail requests that don't comply)
|
||||
*/
|
||||
class TooManyRequest(val retryAfterSeconds: Int) : Http(HTTP_TOO_MANY_REQUESTS, "Too Many Requests")
|
||||
class TooManyRequest(val retryAfterSeconds: Int, proton: ProtonData? = null) :
|
||||
Http(HTTP_TOO_MANY_REQUESTS, "Too Many Requests", proton)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,7 +30,7 @@ class ProtonForceUpdateHandler<Api>(private val apiClient: ApiClient) : ApiError
|
|||
error: ApiResult.Error,
|
||||
call: ApiManager.Call<Api, T>
|
||||
): ApiResult<T> {
|
||||
if (error is ApiResult.Error.Proton && error.protonCode == ERROR_CODE_FORCE_UPDATE)
|
||||
if (error is ApiResult.Error.Http && error.proton?.code == ERROR_CODE_FORCE_UPDATE)
|
||||
apiClient.forceUpdate()
|
||||
return error
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
`kotlin-serialization`
|
||||
}
|
||||
|
||||
libVersion = Version(0, 1, 2)
|
||||
libVersion = Version(0, 1, 3)
|
||||
|
||||
dependencies {
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import kotlinx.serialization.KSerializer
|
|||
import kotlinx.serialization.PrimitiveDescriptor
|
||||
import kotlinx.serialization.PrimitiveKind
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.parse
|
||||
|
@ -50,6 +51,21 @@ inline fun <reified T : Any> String.deserialize(
|
|||
deserializer: DeserializationStrategy<T>? = null
|
||||
): T = deserializer?.let { Serializer.parse(deserializer, this) } ?: Serializer.parse(this)
|
||||
|
||||
/**
|
||||
* @return [T] object from the receiver [String] or null if receiver can't be deserialized to [T].
|
||||
*
|
||||
* Note: this function uses reflection as long as explicit [deserializer] [DeserializationStrategy]
|
||||
* is passed explicitly
|
||||
*/
|
||||
@NeedSerializable
|
||||
inline fun <reified T : Any> String.deserializeOrNull(
|
||||
deserializer: DeserializationStrategy<T>? = null
|
||||
): T? = try {
|
||||
deserialize(deserializer)
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [List] of [T] object from the receiver [String]
|
||||
* This uses reflection: TODO improve for avoid it
|
||||
|
|
Loading…
Reference in New Issue