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.