Skip to content

Commit

Permalink
refactor api client
Browse files Browse the repository at this point in the history
  • Loading branch information
stslex committed Jan 24, 2024
1 parent c0f1972 commit c2e6c44
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 154 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stslex.core.network.api.server.client

import com.stslex.core.network.api.base.NetworkClient

interface ServerApiClient : NetworkClient
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.stslex.core.network.api.server.client

import com.stslex.core.core.AppDispatcher
import com.stslex.core.network.api.server.http_client.ServerHttpClient
import com.stslex.core.network.api.server.model.ErrorRepeatEnd
import io.ktor.client.HttpClient
import kotlinx.coroutines.withContext

class ServerApiClientImpl(
private val appDispatcher: AppDispatcher,
private val client: ServerHttpClient
) : ServerApiClient {

override suspend fun <T> request(
request: suspend HttpClient.() -> T
): T = withContext(appDispatcher.io) {
try {
request(client.authClient)
} catch (error: ErrorRepeatEnd) {
request(client.authClient)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stslex.core.network.api.server.error_handler

import io.ktor.client.plugins.CallRequestExceptionHandler

interface ErrorHandler : CallRequestExceptionHandler
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.stslex.core.network.api.server.error_handler

import com.stslex.core.network.api.server.error_handler.RefreshTokenValidator.setupResponseValidator
import com.stslex.core.network.api.server.http_client.ServerHttpClient
import com.stslex.core.network.api.server.model.ErrorRepeatEnd
import com.stslex.core.network.api.server.model.TokenResponseModel
import com.stslex.core.network.utils.token.AuthController
import com.stslex.core.network.utils.token.toModel
import io.ktor.client.call.body
import io.ktor.client.plugins.ResponseException
import io.ktor.client.request.HttpRequest
import io.ktor.client.request.bearerAuth
import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

class ErrorHandlerImpl(
private val client: Lazy<ServerHttpClient>,
private val tokenProvider: AuthController,
) : ErrorHandler {

private var refreshJob: Job? = null

override suspend fun invoke(cause: Throwable, request: HttpRequest) {
when {
cause !is ResponseException -> throw cause
cause.response.status.value == HttpStatusCode.Unauthorized.value -> refreshToken()
else -> throw cause
}
}

private suspend fun refreshToken() {
if (refreshJob?.isActive == true) return
refreshJob = coroutineScope {
launch {
val tokenResponse = client
.value
.client
.setupResponseValidator()
.get("passport/refresh") {
bearerAuth(tokenProvider.refreshToken)
}
.body<TokenResponseModel>()
tokenProvider.update(tokenResponse.toModel())
// TODO remove error throw after refresh token (move logic out from throwing)
throw ErrorRepeatEnd
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.stslex.core.network.api.server.error_handler

import com.stslex.core.network.api.server.model.ErrorRefresh
import io.ktor.client.HttpClient
import io.ktor.client.plugins.CallRequestExceptionHandler
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.plugins.ResponseException
import io.ktor.client.request.HttpRequest
import io.ktor.http.HttpStatusCode

internal object RefreshTokenValidator : CallRequestExceptionHandler {

fun HttpClient.setupResponseValidator(): HttpClient = config {
HttpResponseValidator {
handleResponseExceptionWithRequest(this@RefreshTokenValidator)
}
}

override suspend fun invoke(cause: Throwable, request: HttpRequest) {
throw when {
cause !is ResponseException -> cause
cause.response.status.value == HttpStatusCode.Unauthorized.value -> ErrorRefresh
else -> cause
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.stslex.core.network.api.server.http_client

import io.ktor.client.HttpClient

interface ServerHttpClient {

val client: HttpClient

val authClient: HttpClient
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.stslex.core.network.api.server.http_client

import Wizard.core.network.BuildConfig
import com.stslex.core.network.api.base.NetworkClientBuilder.setupLogging
import com.stslex.core.network.api.base.NetworkClientBuilder.setupNegotiation
import com.stslex.core.network.api.server.error_handler.ErrorHandler
import com.stslex.core.network.utils.token.AuthController
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.headers
import io.ktor.http.ContentType
import io.ktor.http.URLProtocol
import io.ktor.http.contentType

class ServerHttpClientImpl(
private val errorHandler: ErrorHandler,
private val tokenProvider: AuthController
) : ServerHttpClient {

override val client: HttpClient = HttpClient(CIO) {
setupNegotiation()
setupLogging()
expectSuccess = true
HttpResponseValidator {
handleResponseExceptionWithRequest(errorHandler)
}
defaultRequest {
url(
scheme = URLProtocol.HTTP.name,
host = BuildConfig.SERVER_HOST,
port = BuildConfig.SERVER_PORT.toInt(),
path = BuildConfig.SERVER_API_VERSION,
block = {
contentType(ContentType.Application.Json)
}
)
headers {
append(API_KEY_NAME, BuildConfig.SERVER_API_KEY)
}
}
}

override val authClient: HttpClient
get() = client.config {
install(Auth) {
bearer {
loadTokens {
BearerTokens(
accessToken = tokenProvider.accessToken,
refreshToken = tokenProvider.refreshToken
)
}
}
}
}

companion object {
private const val API_KEY_NAME = "X-Api-Key"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stslex.core.network.api.server.model

/**
* Error refresh token response
* @see ServerApiClientImpl.refreshToken
* @see ServerApiClientImpl.errorParser
* @see ServerApiClientImpl.request
*/
data object ErrorRefresh : Throwable()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.stslex.core.network.api.server.model

/**
* Error repeat request.
* Show that the request was repeated after a refresh token
* @see ServerApiClientImpl.request
*/
internal data object ErrorRepeatEnd : Throwable()
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.stslex.core.network.api.server
package com.stslex.core.network.api.server.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.stslex.core.network.clients.auth.client

import com.stslex.core.network.api.server.ServerApiClient
import com.stslex.core.network.api.server.client.ServerApiClient
import com.stslex.core.network.clients.auth.request.LoginRequest
import com.stslex.core.network.clients.auth.request.RegisterRequest
import com.stslex.core.network.clients.auth.response.LoginOkResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.stslex.core.network.clients.profile.client

import com.stslex.core.network.api.server.ServerApiClient
import com.stslex.core.network.api.server.client.ServerApiClient
import com.stslex.core.network.clients.profile.model.ProfileResponse
import io.ktor.client.call.body
import io.ktor.client.request.get
Expand Down
Loading

0 comments on commit c2e6c44

Please sign in to comment.