diff --git a/core/common/src/main/java/com/acon/acon/core/common/AuthQualifiers.kt b/core/common/src/main/java/com/acon/acon/core/common/AuthQualifiers.kt index a26698d73..649a7f3e4 100644 --- a/core/common/src/main/java/com/acon/acon/core/common/AuthQualifiers.kt +++ b/core/common/src/main/java/com/acon/acon/core/common/AuthQualifiers.kt @@ -18,10 +18,6 @@ annotation class Naver @Retention(AnnotationRetention.BINARY) annotation class TokenInterceptor -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class ResponseInterceptor - @Qualifier @Retention(AnnotationRetention.BINARY) annotation class NaverAuthInterceptor diff --git a/data/src/main/kotlin/com/acon/acon/data/di/NetworkModule.kt b/data/src/main/kotlin/com/acon/acon/data/di/NetworkModule.kt index 5ee5f5590..fe8739ec0 100644 --- a/data/src/main/kotlin/com/acon/acon/data/di/NetworkModule.kt +++ b/data/src/main/kotlin/com/acon/acon/data/di/NetworkModule.kt @@ -1,18 +1,16 @@ package com.acon.acon.data.di import android.content.Context -import com.acon.acon.data.BuildConfig -import com.acon.acon.data.datasource.local.TokenLocalDataSource -import com.acon.acon.data.error.NetworkErrorResponse -import com.acon.acon.data.error.RemoteError -import com.acon.acon.data.remote.ReissueTokenApi import com.acon.acon.core.common.Auth import com.acon.acon.core.common.Naver import com.acon.acon.core.common.NaverAuthInterceptor import com.acon.acon.core.common.NoAuth -import com.acon.acon.core.common.ResponseInterceptor import com.acon.acon.core.common.TokenInterceptor +import com.acon.acon.data.BuildConfig import com.acon.acon.data.SessionManager +import com.acon.acon.data.datasource.local.TokenLocalDataSource +import com.acon.acon.data.error.RemoteErrorCallAdapterFactory +import com.acon.acon.data.remote.ReissueTokenApi import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides @@ -59,7 +57,6 @@ internal object NetworkModule { @Singleton @Provides fun provideAuthClient( - @ResponseInterceptor responseInterceptor: Interceptor, @TokenInterceptor authInterceptor: Interceptor, refreshAuthenticator: Authenticator, ): OkHttpClient { @@ -74,7 +71,6 @@ internal object NetworkModule { }) } } - .addInterceptor(responseInterceptor) .addInterceptor(authInterceptor) .authenticator(refreshAuthenticator) .build() @@ -83,9 +79,7 @@ internal object NetworkModule { @NoAuth @Singleton @Provides - fun provideNoAuthClient( - @ResponseInterceptor responseInterceptor: Interceptor - ): OkHttpClient { + fun provideNoAuthClient(): OkHttpClient { return OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) @@ -96,7 +90,7 @@ internal object NetworkModule { level = HttpLoggingInterceptor.Level.BODY }) } - }.addInterceptor(responseInterceptor) + } .build() } @@ -127,6 +121,7 @@ internal object NetworkModule { .baseUrl(BuildConfig.BASE_URL) .client(client) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .addCallAdapterFactory(RemoteErrorCallAdapterFactory(json)) .build() } @@ -182,39 +177,4 @@ internal object NetworkModule { reissueTokenApi: ReissueTokenApi, sessionManager: SessionManager ): Authenticator = AuthAuthenticator(tokenLocalDataSource, reissueTokenApi, sessionManager) - - @ResponseInterceptor - @Singleton - @Provides - fun providesResponseInterceptor() : Interceptor { - return Interceptor { chain: Interceptor.Chain -> - val response = chain.proceed(chain.request()) - - if (response.isSuccessful.not()) { // response 실패 시 실행 - val errorBody = response.body?.string() - val errorResponse = try { - errorBody?.let { - Json.decodeFromString(it) - } - } catch (e: Exception) { - null - } - - throw RemoteError( - statusCode = response.code, - errorCode = errorResponse?.code ?: 0, - message = errorResponse?.message ?: response.message, - httpErrorMessage = when(response.code) { - 400 -> "Bad Request: 잘못된 요청입니다." - 401 -> "Unauthorized: 인증되지 않은 사용자입니다." - 403 -> "Forbidden: 접근 권한이 없습니다." - 404 -> "Not Found: 요청한 리소스를 찾을 수 없습니다." - in 500 until 600 -> "Internal Server Error: 서버 내부 오류입니다." - else -> "Unknown Error: 알 수 없는 오류입니다." - }, - ) - } - response - } - } } diff --git a/data/src/main/kotlin/com/acon/acon/data/error/ErrorCallAdapter.kt b/data/src/main/kotlin/com/acon/acon/data/error/ErrorCallAdapter.kt new file mode 100644 index 000000000..f73273bd6 --- /dev/null +++ b/data/src/main/kotlin/com/acon/acon/data/error/ErrorCallAdapter.kt @@ -0,0 +1,100 @@ +package com.acon.acon.data.error + +import kotlinx.serialization.json.Json +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class RemoteErrorCallAdapterFactory( + private val json: Json = Json +) : CallAdapter.Factory() { + + override fun get( + returnType: Type, annotations: Array, retrofit: Retrofit + ): CallAdapter>? { + if (getRawType(returnType) != Call::class.java) return null + + val responseType = (returnType as ParameterizedType).actualTypeArguments[0] + return RemoteErrorCallAdapter(responseType, json) + } + + private class RemoteErrorCallAdapter( + private val responseType: Type, private val json: Json + ) : CallAdapter> { + + override fun responseType(): Type = responseType + + override fun adapt(call: Call): Call { + return object : Call { + override fun enqueue(callback: Callback) { + call.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + if (response.body() != null) + callback.onResponse(call, response) + else { + callback.onFailure( + call, RemoteError( + response = response, + errorCode = 0, + message = "Empty body", + ) + ) + } + } else { + val errJson = response.errorBody()?.string() + val errResp = try { + errJson?.let { json.decodeFromString(it) } + } catch (_: Exception) { + null + } + callback.onFailure( + call, RemoteError( + response = response, + errorCode = errResp?.code ?: 0, + message = errResp?.message ?: response.message(), + ) + ) + } + } + + override fun onFailure(call: Call, t: Throwable) { + callback.onFailure(call, t) + } + }) + } + + override fun execute(): Response { + val response = call.execute() + if (response.isSuccessful) return response + val errJson = response.errorBody()?.string() + val errResp = try { + errJson?.let { json.decodeFromString(it) } + } catch (_: Exception) { + null + } + throw RemoteError( + response = response, + errorCode = errResp?.code ?: 0, + message = errResp?.message ?: response.message(), + ) + } + + override fun clone(): Call = adapt(call.clone()) + override fun isExecuted(): Boolean = call.isExecuted + override fun cancel() = call.cancel() + override fun isCanceled(): Boolean = call.isCanceled + override fun request(): Request = call.request() + override fun timeout(): Timeout = call.timeout() + + + } + } + } +} diff --git a/data/src/main/kotlin/com/acon/acon/data/error/ErrorUtils.kt b/data/src/main/kotlin/com/acon/acon/data/error/ErrorUtils.kt index 1af79f6da..6267dd6f2 100644 --- a/data/src/main/kotlin/com/acon/acon/data/error/ErrorUtils.kt +++ b/data/src/main/kotlin/com/acon/acon/data/error/ErrorUtils.kt @@ -1,6 +1,7 @@ package com.acon.acon.data.error import com.acon.acon.domain.error.RootError +import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException internal inline fun runCatchingWith( @@ -10,12 +11,14 @@ internal inline fun runCatchingWith( return try { Result.success(block()) } catch (e: RemoteError) { + Timber.d("RemoteError: $e") definedErrors.find { definedError -> e.errorCode == definedError.code }?.let { Result.failure(it) } ?: Result.failure(e) } catch (e: CancellationException) { throw e } catch (e: Throwable) { + Timber.d("Throwable: $e") Result.failure(e) } } \ No newline at end of file diff --git a/data/src/main/kotlin/com/acon/acon/data/error/RemoteError.kt b/data/src/main/kotlin/com/acon/acon/data/error/RemoteError.kt index 6c1ee452a..4645edd5c 100644 --- a/data/src/main/kotlin/com/acon/acon/data/error/RemoteError.kt +++ b/data/src/main/kotlin/com/acon/acon/data/error/RemoteError.kt @@ -1,10 +1,26 @@ package com.acon.acon.data.error -import java.io.IOException +import retrofit2.HttpException +import retrofit2.Response +/** + * 400, 500번대 에러 발생 시 던져지는 에러 + */ data class RemoteError( - val statusCode: Int, + val response: Response<*>, val errorCode: Int, override val message: String, - val httpErrorMessage: String -) : IOException() \ No newline at end of file +) : HttpException(response) { + + val statusCode: Int = response.code() + val httpErrorMessage: String = mapHttpError(statusCode) +} + +private fun mapHttpError(code: Int) = when (code) { + 400 -> "Bad Request: 잘못된 요청입니다." + 401 -> "Unauthorized: 인증되지 않은 사용자입니다." + 403 -> "Forbidden: 접근 권한이 없습니다." + 404 -> "Not Found: 요청한 리소스를 찾을 수 없습니다." + in 500 until 600 -> "Internal Server Error: 서버 내부 오류입니다." + else -> "Unknown Error: 알 수 없는 오류입니다." +}