Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RefreshTokenPlugin: excecution is locked when updateTokenHandler gets 401 error #205

Open
RezMike opened this issue Aug 4, 2023 · 1 comment

Comments

@RezMike
Copy link

RezMike commented Aug 4, 2023

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
Copy link
Author

RezMike commented Aug 4, 2023

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")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant