android: unify api error mapping across data repositories
Some checks failed
CI / test (push) Failing after 2m13s
Some checks failed
CI / test (push) Failing after 2m13s
This commit is contained in:
@@ -333,3 +333,8 @@
|
|||||||
|
|
||||||
### Step 55 - Chat multi-select action cleanup
|
### Step 55 - Chat multi-select action cleanup
|
||||||
- Removed duplicate forward action in multi-select mode (`Forward selected`), leaving a single clear forward action button.
|
- Removed duplicate forward action in multi-select mode (`Forward selected`), leaving a single clear forward action button.
|
||||||
|
|
||||||
|
### Step 56 - Unified API error handling
|
||||||
|
- Added shared API error mapper (`ApiErrorMapper`) with mode-aware mapping (`DEFAULT`, `LOGIN`).
|
||||||
|
- Switched auth/chat/message/media repositories to a single `Throwable -> AppError` mapping source.
|
||||||
|
- Kept login-specific invalid-credentials mapping while standardizing unauthorized/server/network handling for other API calls.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package ru.daemonlord.messenger.data.auth.repository
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import retrofit2.HttpException
|
|
||||||
import ru.daemonlord.messenger.core.token.TokenBundle
|
import ru.daemonlord.messenger.core.token.TokenBundle
|
||||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||||
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
import ru.daemonlord.messenger.data.auth.api.AuthApiService
|
||||||
@@ -10,13 +9,14 @@ import ru.daemonlord.messenger.data.auth.dto.AuthUserDto
|
|||||||
import ru.daemonlord.messenger.data.auth.dto.AuthSessionDto
|
import ru.daemonlord.messenger.data.auth.dto.AuthSessionDto
|
||||||
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
|
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
|
||||||
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
|
||||||
|
import ru.daemonlord.messenger.data.common.ApiErrorMode
|
||||||
|
import ru.daemonlord.messenger.data.common.toAppError
|
||||||
import ru.daemonlord.messenger.di.IoDispatcher
|
import ru.daemonlord.messenger.di.IoDispatcher
|
||||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||||
import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
||||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||||
import ru.daemonlord.messenger.domain.common.AppError
|
import ru.daemonlord.messenger.domain.common.AppError
|
||||||
import ru.daemonlord.messenger.domain.common.AppResult
|
import ru.daemonlord.messenger.domain.common.AppResult
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
getMe()
|
getMe()
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
AppResult.Error(error.toAppError(forLogin = true))
|
AppResult.Error(error.toAppError(mode = ApiErrorMode.LOGIN))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
AppResult.Success(Unit)
|
AppResult.Success(Unit)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
tokenRepository.clearTokens()
|
tokenRepository.clearTokens()
|
||||||
AppResult.Error(error.toAppError(forLogin = false))
|
AppResult.Error(error.toAppError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
val user = authApiService.me().toDomain()
|
val user = authApiService.me().toDomain()
|
||||||
AppResult.Success(user)
|
AppResult.Success(user)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
AppResult.Error(error.toAppError(forLogin = false))
|
AppResult.Error(error.toAppError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
AppResult.Success(authApiService.sessions().map { it.toDomain() })
|
AppResult.Success(authApiService.sessions().map { it.toDomain() })
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
AppResult.Error(error.toAppError(forLogin = false))
|
AppResult.Error(error.toAppError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
authApiService.revokeSession(jti = jti)
|
authApiService.revokeSession(jti = jti)
|
||||||
AppResult.Success(Unit)
|
AppResult.Success(Unit)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
AppResult.Error(error.toAppError(forLogin = false))
|
AppResult.Error(error.toAppError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
authApiService.revokeAllSessions()
|
authApiService.revokeAllSessions()
|
||||||
AppResult.Success(Unit)
|
AppResult.Success(Unit)
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
AppResult.Error(error.toAppError(forLogin = false))
|
AppResult.Error(error.toAppError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,15 +150,4 @@ class NetworkAuthRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.toAppError(forLogin: Boolean): AppError {
|
|
||||||
return when (this) {
|
|
||||||
is IOException -> AppError.Network
|
|
||||||
is HttpException -> when (code()) {
|
|
||||||
400 -> if (forLogin) AppError.InvalidCredentials else AppError.Server(message = message())
|
|
||||||
401, 403 -> if (forLogin) AppError.InvalidCredentials else AppError.Unauthorized
|
|
||||||
else -> AppError.Server(message = message())
|
|
||||||
}
|
|
||||||
else -> AppError.Unknown(cause = this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,18 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import retrofit2.HttpException
|
|
||||||
import ru.daemonlord.messenger.data.chat.api.ChatApiService
|
import ru.daemonlord.messenger.data.chat.api.ChatApiService
|
||||||
import ru.daemonlord.messenger.data.chat.dto.ChatJoinByInviteRequestDto
|
import ru.daemonlord.messenger.data.chat.dto.ChatJoinByInviteRequestDto
|
||||||
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
||||||
import ru.daemonlord.messenger.data.chat.mapper.toChatEntity
|
import ru.daemonlord.messenger.data.chat.mapper.toChatEntity
|
||||||
import ru.daemonlord.messenger.data.chat.mapper.toDomain
|
import ru.daemonlord.messenger.data.chat.mapper.toDomain
|
||||||
import ru.daemonlord.messenger.data.chat.mapper.toUserShortEntityOrNull
|
import ru.daemonlord.messenger.data.chat.mapper.toUserShortEntityOrNull
|
||||||
|
import ru.daemonlord.messenger.data.common.toAppError
|
||||||
import ru.daemonlord.messenger.domain.chat.model.ChatInviteLink
|
import ru.daemonlord.messenger.domain.chat.model.ChatInviteLink
|
||||||
import ru.daemonlord.messenger.di.IoDispatcher
|
import ru.daemonlord.messenger.di.IoDispatcher
|
||||||
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
import ru.daemonlord.messenger.domain.chat.model.ChatItem
|
||||||
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
|
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
|
||||||
import ru.daemonlord.messenger.domain.common.AppError
|
|
||||||
import ru.daemonlord.messenger.domain.common.AppResult
|
import ru.daemonlord.messenger.domain.common.AppResult
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -102,16 +100,4 @@ class NetworkChatRepository @Inject constructor(
|
|||||||
chatDao.deleteChat(chatId = chatId)
|
chatDao.deleteChat(chatId = chatId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.toAppError(): AppError {
|
|
||||||
return when (this) {
|
|
||||||
is IOException -> AppError.Network
|
|
||||||
is HttpException -> if (code() == 401 || code() == 403) {
|
|
||||||
AppError.Unauthorized
|
|
||||||
} else {
|
|
||||||
AppError.Server(message = message())
|
|
||||||
}
|
|
||||||
else -> AppError.Unknown(cause = this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ru.daemonlord.messenger.data.common
|
||||||
|
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import ru.daemonlord.messenger.domain.common.AppError
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
enum class ApiErrorMode {
|
||||||
|
DEFAULT,
|
||||||
|
LOGIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Throwable.toAppError(mode: ApiErrorMode = ApiErrorMode.DEFAULT): AppError {
|
||||||
|
return when (this) {
|
||||||
|
is IOException -> AppError.Network
|
||||||
|
is HttpException -> when (mode) {
|
||||||
|
ApiErrorMode.LOGIN -> when (code()) {
|
||||||
|
400, 401, 403 -> AppError.InvalidCredentials
|
||||||
|
else -> AppError.Server(message = message())
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiErrorMode.DEFAULT -> if (code() == 401 || code() == 403) {
|
||||||
|
AppError.Unauthorized
|
||||||
|
} else {
|
||||||
|
AppError.Server(message = message())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> AppError.Unknown(cause = this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import retrofit2.HttpException
|
import ru.daemonlord.messenger.data.common.toAppError
|
||||||
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
||||||
import ru.daemonlord.messenger.data.media.dto.AttachmentCreateRequestDto
|
import ru.daemonlord.messenger.data.media.dto.AttachmentCreateRequestDto
|
||||||
import ru.daemonlord.messenger.data.media.dto.UploadUrlRequestDto
|
import ru.daemonlord.messenger.data.media.dto.UploadUrlRequestDto
|
||||||
@@ -15,7 +15,6 @@ import ru.daemonlord.messenger.di.RefreshClient
|
|||||||
import ru.daemonlord.messenger.domain.common.AppError
|
import ru.daemonlord.messenger.domain.common.AppError
|
||||||
import ru.daemonlord.messenger.domain.common.AppResult
|
import ru.daemonlord.messenger.domain.common.AppResult
|
||||||
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -71,16 +70,4 @@ class NetworkMediaRepository @Inject constructor(
|
|||||||
AppResult.Error(error.toAppError())
|
AppResult.Error(error.toAppError())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.toAppError(): AppError {
|
|
||||||
return when (this) {
|
|
||||||
is IOException -> AppError.Network
|
|
||||||
is HttpException -> if (code() == 401 || code() == 403) {
|
|
||||||
AppError.Unauthorized
|
|
||||||
} else {
|
|
||||||
AppError.Server(message = message())
|
|
||||||
}
|
|
||||||
else -> AppError.Unknown(cause = this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,16 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import retrofit2.HttpException
|
|
||||||
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
||||||
import ru.daemonlord.messenger.data.message.api.MessageApiService
|
import ru.daemonlord.messenger.data.message.api.MessageApiService
|
||||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||||
|
import ru.daemonlord.messenger.data.common.toAppError
|
||||||
import ru.daemonlord.messenger.data.message.local.dao.MessageDao
|
import ru.daemonlord.messenger.data.message.local.dao.MessageDao
|
||||||
import ru.daemonlord.messenger.data.message.local.entity.MessageEntity
|
import ru.daemonlord.messenger.data.message.local.entity.MessageEntity
|
||||||
import ru.daemonlord.messenger.data.message.mapper.toDomain
|
import ru.daemonlord.messenger.data.message.mapper.toDomain
|
||||||
import ru.daemonlord.messenger.data.message.mapper.toEntity
|
import ru.daemonlord.messenger.data.message.mapper.toEntity
|
||||||
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
import ru.daemonlord.messenger.data.media.api.MediaApiService
|
||||||
import ru.daemonlord.messenger.di.IoDispatcher
|
import ru.daemonlord.messenger.di.IoDispatcher
|
||||||
import ru.daemonlord.messenger.domain.common.AppError
|
|
||||||
import ru.daemonlord.messenger.domain.common.AppResult
|
import ru.daemonlord.messenger.domain.common.AppResult
|
||||||
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
||||||
import ru.daemonlord.messenger.domain.message.model.MessageItem
|
import ru.daemonlord.messenger.domain.message.model.MessageItem
|
||||||
@@ -30,7 +29,6 @@ import ru.daemonlord.messenger.data.message.dto.MessageForwardBulkRequestDto
|
|||||||
import ru.daemonlord.messenger.data.message.dto.MessageReactionToggleRequestDto
|
import ru.daemonlord.messenger.data.message.dto.MessageReactionToggleRequestDto
|
||||||
import ru.daemonlord.messenger.data.message.dto.MessageStatusUpdateRequestDto
|
import ru.daemonlord.messenger.data.message.dto.MessageStatusUpdateRequestDto
|
||||||
import ru.daemonlord.messenger.data.message.dto.MessageUpdateRequestDto
|
import ru.daemonlord.messenger.data.message.dto.MessageUpdateRequestDto
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -382,18 +380,6 @@ class NetworkMessageRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.toAppError(): AppError {
|
|
||||||
return when (this) {
|
|
||||||
is IOException -> AppError.Network
|
|
||||||
is HttpException -> if (code() == 401 || code() == 403) {
|
|
||||||
AppError.Unauthorized
|
|
||||||
} else {
|
|
||||||
AppError.Server(message = message())
|
|
||||||
}
|
|
||||||
else -> AppError.Unknown(cause = this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.extractUserIdFromJwt(): Long? {
|
private fun String.extractUserIdFromJwt(): Long? {
|
||||||
val payloadPart = split('.').getOrNull(1) ?: return null
|
val payloadPart = split('.').getOrNull(1) ?: return null
|
||||||
val normalized = payloadPart
|
val normalized = payloadPart
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ru.daemonlord.messenger.data.common
|
||||||
|
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import retrofit2.Response
|
||||||
|
import ru.daemonlord.messenger.domain.common.AppError
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class ApiErrorMapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ioException_mapsToNetwork() {
|
||||||
|
val error = IOException("offline").toAppError()
|
||||||
|
assertTrue(error is AppError.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loginMode_maps401ToInvalidCredentials() {
|
||||||
|
val error = httpException(401).toAppError(mode = ApiErrorMode.LOGIN)
|
||||||
|
assertTrue(error is AppError.InvalidCredentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun defaultMode_maps401ToUnauthorized() {
|
||||||
|
val error = httpException(401).toAppError(mode = ApiErrorMode.DEFAULT)
|
||||||
|
assertTrue(error is AppError.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun defaultMode_maps500ToServer() {
|
||||||
|
val error = httpException(500).toAppError(mode = ApiErrorMode.DEFAULT)
|
||||||
|
assertTrue(error is AppError.Server)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun httpException(code: Int): HttpException {
|
||||||
|
val body = "{}".toResponseBody("application/json".toMediaType())
|
||||||
|
return HttpException(Response.error<Any>(code, body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
## 2. Сеть и API
|
## 2. Сеть и API
|
||||||
- [x] Retrofit/OkHttp + auth interceptor
|
- [x] Retrofit/OkHttp + auth interceptor
|
||||||
- [x] Авто-refresh JWT
|
- [x] Авто-refresh JWT
|
||||||
- [ ] Единая обработка ошибок API
|
- [x] Единая обработка ошибок API
|
||||||
- [x] Realtime WebSocket слой (reconnect/backoff)
|
- [x] Realtime WebSocket слой (reconnect/backoff)
|
||||||
- [x] Маппинг DTO -> Domain -> UI models
|
- [x] Маппинг DTO -> Domain -> UI models
|
||||||
- [ ] Версионирование API и feature flags
|
- [ ] Версионирование API и feature flags
|
||||||
|
|||||||
Reference in New Issue
Block a user