moko-network icon indicating copy to clipboard operation
moko-network copied to clipboard

RefreshTokenPlugin: excecution is locked when updateTokenHandler gets 401 error

Open RezMike opened this issue 2 years ago • 1 comments

updateTokenHandler usually contains a request for refreshing accessToken, but in some APIs this request can get 401 Unauthorized error (for example, when refreshToken is outdated as well). In this case RefreshTokenPlugin gets locked by mutex, and no other requests can be executed

RezMike avatar Aug 04 '23 06:08 RezMike

Possible solution - add needSkipRequest lambda as another parameter in RefreshTokenPlugin:

class RefreshTokenPlugin(
    private val updateTokenHandler: suspend () -> Boolean,
    private val isCredentialsActual: (HttpRequest) -> Boolean,
    private val needSkipRequest: (HttpRequest) -> Boolean,
) {

    class Config {
        var updateTokenHandler: (suspend () -> Boolean)? = null
        var isCredentialsActual: ((HttpRequest) -> Boolean)? = null
        var needSkipRequest: (HttpRequest) -> Boolean = { false }

        fun build() = RefreshTokenPlugin(
            updateTokenHandler = updateTokenHandler
                ?: throw IllegalArgumentException("updateTokenHandler should be passed"),
            isCredentialsActual = isCredentialsActual
                ?: throw IllegalArgumentException("isCredentialsActual should be passed"),
            needSkipRequest = needSkipRequest,
        )
    }

    companion object Plugin : HttpClientPlugin<Config, RefreshTokenPlugin> {

        private val refreshTokenHttpPluginMutex = Mutex()

        override val key = AttributeKey<RefreshTokenPlugin>("RefreshTokenPlugin")

        override fun prepare(block: Config.() -> Unit) = Config().apply(block).build()

        override fun install(plugin: RefreshTokenPlugin, scope: HttpClient) {
            scope.receivePipeline.intercept(HttpReceivePipeline.After) {
                if (subject.status != HttpStatusCode.Unauthorized || plugin.needSkipRequest(subject.request)) {
                    proceedWith(subject)
                    return@intercept
                }

                refreshTokenHttpPluginMutex.withLock {
                    // If token of the request isn't actual, then token has already been updated and
                    // let's just to try repeat request
                    if (!plugin.isCredentialsActual(subject.request)) {
                        val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
                        val result: HttpResponse = scope.request(requestBuilder)
                        proceedWith(result)
                        return@intercept
                    }

                    // Else if token of the request is actual (same as in the storage), then need to send
                    // refresh request.
                    if (plugin.updateTokenHandler.invoke()) {
                        // If the request refresh was successful, then let's just to try repeat request
                        val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
                        val result: HttpResponse = scope.request(requestBuilder)
                        proceedWith(result)
                    } else {
                        // If the request refresh was unsuccessful
                        proceedWith(subject)
                    }
                }
            }
        }
    }
}

Example usage:

needSkipRequest = { request ->
    request.call.request.url.encodedPath.endsWith("/auth/refresh")
}

RezMike avatar Aug 04 '23 06:08 RezMike