Ktor Retry failed request manually is not working

38 views Asked by At

I am trying to manually handling refresh token logic in Ktor and its working fine but the problem I am facing is once I get the response from refresh token API how to re-request the failed API? Its executing twice the failed API before hitting refreshtoken api. Don't know why. I tried the below code but its getting struck with continue loop.

Below is the function used to call API

    @Throws(AppException::class)
    suspend inline fun <reified T> get(
        block: HttpRequestBuilder.() -> Unit = {}
    ): T = try {
        request(block)
    } catch (e: AppException) {
        throw e
    }

Above function internally call request function below

    @PublishedApi
    internal suspend inline fun <reified T> request(
        block: HttpRequestBuilder.() -> Unit
    ): T = defaultHttpClient
        .request(
            HttpRequestBuilder()
                .apply {
                    headersMap().forEach { (key, value) -> header(key, value) }
                    authToken().accessToken?.let { header(AUTHORIZATION, it) }
                }.apply(block)
        ).body()

'defaultHttpClient' is below

private fun httpClient() = HttpClient(engine = configuration.engine()) {
        configuration.requestRetry?.let { config ->
            val configuration: HttpRequestRetryConfig = HttpRequestRetryConfig().apply(config)

            install(HttpRequestRetry) {
                configuration.maxRetry?.let { maxRetries ->
                    val max = maxRetries.invoke()
                    /**
                    retryOnException: It will retry on all exception except cancellation exception
                     */
                    retryOnException(
                        maxRetries = max,
                        retryOnTimeout = configuration.retryOnTimeout.invoke()
                    )

                    /**
                    retry if status is not success: it will retry is status code is not [200, 300]
                     */
                    retryIf(maxRetries = max) { _, response ->
                        !response.status.isSuccess()
                    }
                }
                exponentialDelay()
                modifyRequest {
                    request.headers.append("x-retry-count", retryCount.toString())
                }
            }
        }
        install(ContentNegotiation) {
            json(
                Json {
                    ignoreUnknownKeys = true
                    encodeDefaults = true
                    isLenient = true
                    prettyPrint = true
                    explicitNulls = false
                },
            )
        }

        Logging {
            logger = object : Logger {
                override fun log(message: String) {
                    if (configuration.logging) {
                        httpMetrics.log(message)
                    }
                }
            }
            level = LogLevel.ALL
        }

        install(HttpTimeout) {
            requestTimeoutMillis = timeoutConfig.requestTimeoutMillis
            connectTimeoutMillis = timeoutConfig.connectTimeoutMillis
            socketTimeoutMillis = timeoutConfig.socketTimeoutMillis
        }

        defaultRequest {
            url {
                protocol = URLProtocol.createOrDefault(environment.protocol)
                host = environment.baseUrl
            }
            contentType(configuration.contentType)
        }
        HttpResponseValidator {
            validateResponse { response ->
                val statusCode = response.status
                val originCall = response.call
                if (statusCode.value < 300 || originCall.attributes.contains(ValidateMark)) return@validateResponse

                val exceptionCall = originCall.save().apply {
                    attributes.put(ValidateMark, Unit)
                }

                when (statusCode) {
                    HttpStatusCode.NoContent -> {
                        throw AppException(
                            ErrorResponse(
                                HttpStatusCode.NoContent.value.toString(),
                                HttpStatusCode.NoContent.description
                            )
                        )
                    }

                    HttpStatusCode.BadRequest -> {
                        if (response.request.url.fullPath.contains("refreshToken")) {
                            // logout user if in refreshToken api we get 400
                            userManager.logout()
                            return@validateResponse
                        }
                    }

                    HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> {
                        if (response.request.url.fullPath.contains("refreshToken")) {
                            // logout user if in refreshToken api we get 401 or 403
                            userManager.logout()
                            return@validateResponse
                        } else {
                            val refreshTokenResponse: RefreshTokenModel = request(
                                RefreshToken(
                                    refreshToken = authToken().refreshToken.orEmpty(),
                                    email = authToken().workEmail.orEmpty(),
                                    deviceId = deviceId.orEmpty()
                                )
                            )

//                             Once user get the valid response form refresh token api
//                             then again hit the failed API.
                            val req: HttpRequestBuilder.() -> Unit = {
                                url {
                                    path(originCall.request.url.fullPath)
                                }
//                                headers[HttpHeaders.Authorization] = "Bearer ${refreshTokenResponse.token}"
                                refreshTokenResponse.token?.let { bearerAuth(it) }

                                setBody(originCall.request.content)
                                method = originCall.request.method
                            }
                            originCall.client.request(HttpRequestBuilder().apply(req))
                            return@validateResponse
                        }
                    }
                }

                val exceptionResponse = exceptionCall.response
                val exceptionResponseText = try {
                    exceptionResponse.bodyAsText()
                } catch (_: MalformedInputException) {
                    BODY_FAILED_DECODING
                }
                val errorResponse = Json.decodeFromString<ErrorResponse>(exceptionResponseText)
                throw AppException(errorResponse)
            }
        }
    }

My UseCase

  • If we hit an API request and it throws 401 or 403 we need to refresh the token
  • Once we get the successful response from refresh token API
  • We again continue the failed API

I am trying originCall.client.request(HttpRequestBuilder().apply(req)) is the line I am using to retrying the failed request and its working as expected as well but the problem is failed request is getting executed twice because of which its strucked with infinite loop.

0

There are 0 answers