ursus
04/23/2026, 10:50 PMokhttp how do I trigger Authenticator if access tokens are deemed expired locally? (to save on traffic that will most likely end up in 401 - and to refresh & retry right away)
class AccessTokenInterceptor(private val provider: Provider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val accessToken = provider.get()
return if (accessToken == null) {
chain.proceed(request)
} else {
if (accessToken.isExpired(clock)) {
Response.Builder() <----------------------------------
.protocol(Protocol.HTTP_1_1)
.message("foo")
.request(request)
.code(401)
.build()
} else {
chain.proceed(
request
.newBuilder
.header("Authorization", "Bearer ${accessToken.value}")
.build()
)
}
}
}
I'm trying to return a dummy 401 response but Authenticator is not triggeredyschimke
04/24/2026, 5:41 AMursus
04/24/2026, 8:38 AMappOkHttp.newBuilder()
.addInterceptor(AccessTokenInterceptor(...))
.authenticator(AccessTokenAuthenticator(...))
.build()
should it?ursus
04/24/2026, 8:43 AMnetworkInterceptor and now I'm getting
java.io.IOException: canceled due to java.lang.IllegalStateException: network interceptor sk.o2.auth.interceptor.AccessTokenInterceptor@97f9958 must call proceed() exactly once
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:587)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
which means..I cannot not call proceed, so I have to make the actual request?yschimke
04/24/2026, 8:45 AMyschimke
04/24/2026, 8:47 AMursus
04/24/2026, 8:48 AMyschimke
04/24/2026, 8:49 AMursus
04/24/2026, 8:50 AMyschimke
04/24/2026, 8:51 AMyschimke
04/24/2026, 8:52 AMursus
04/24/2026, 8:52 AMokhttp3.Authenticator?yschimke
04/24/2026, 8:55 AMyschimke
04/24/2026, 8:56 AMursus
04/24/2026, 8:56 AMursus
04/24/2026, 8:59 AMclass AccessTokenInterceptor(
private val provider: Provider,
private val authenticator: Authenticator,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val accessToken = provider.get()
return if (accessToken.isExpired) {
val synthetic401Response = Response.Builder()
.protocol(Protocol.HTTP_1_1)
.message("Synthetic unauthorized")
.request(request)
.code(401)
.build()
val reauthRequest = authenticator.authenticate(
route = chain.connection()?.route(),
response = synthetic401Response <-------------
)
if (reauthRequest == null) {
synthetic401Response <-------------- specifically this bit
} else {
chain.proceed(reauthRequest)
}
} else {
chain.proceed(request.authorized(accessToken, isReauth = false))
}
}
interface Provider {
fun get(): AccessToken?
}
}
does this look right to you please?
responses need to be closed right?jessewilson
04/24/2026, 11:43 AMjessewilson
04/24/2026, 11:43 AMjessewilson
04/24/2026, 11:44 AMursus
04/24/2026, 11:46 AMjessewilson
04/24/2026, 12:33 PMjessewilson
04/24/2026, 12:33 PMursus
04/24/2026, 12:37 PMjessewilson
04/24/2026, 1:57 PMursus
04/24/2026, 2:07 PMtoken.expiry?
(My actual case is that backends changed identity provider and now access tokens are only 5minutes long, so they're getting a lot of requests from the app that end up 401'ed - and would me to not cause such traffic, refresh right away if I'm locally confident it's expired)
But it's just a optimization, not a hard requirement, reacting to actual 401 still should work as standardjessewilson
04/24/2026, 2:33 PMjessewilson
04/24/2026, 2:34 PMjessewilson
04/24/2026, 2:35 PMursus
04/24/2026, 2:36 PMjessewilson
04/24/2026, 2:36 PMursus
04/24/2026, 2:36 PMjessewilson
04/24/2026, 2:36 PMursus
04/24/2026, 2:37 PM