Skip to content

Commit a56c81e

Browse files
Update interceptor correctly
1 parent c7191a2 commit a56c81e

File tree

2 files changed

+82
-92
lines changed

2 files changed

+82
-92
lines changed

stream-android-core/src/main/java/io/getstream/android/core/internal/http/interceptor/StreamAuthInterceptor.kt

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -61,58 +61,46 @@ internal class StreamAuthInterceptor(
6161
}
6262

6363
override fun intercept(chain: Interceptor.Chain): Response {
64-
val token =
65-
runBlocking { tokenManager.loadIfAbsent() }
66-
.getOrEndpointException("Failed to load token.")
64+
val token = runBlocking { tokenManager.loadIfAbsent() }.getOrEndpointException("Failed to load token.")
6765
val original = chain.request()
6866
val authed = original.withAuthHeaders(authType, token.rawValue)
6967

70-
val firstResponse = chain.proceed(authed)
71-
if (firstResponse.isSuccessful) {
72-
return firstResponse
73-
}
68+
val first = chain.proceed(authed)
69+
if (first.isSuccessful) return first
7470

75-
val errorBody = firstResponse.peekBody(PEEK_ERROR_BYTES_MAX).string()
76-
val parsed = jsonParser.fromJson(errorBody, StreamEndpointErrorData::class.java)
71+
// Peek only; do NOT consume
72+
val peeked = first.peekBody(PEEK_ERROR_BYTES_MAX).string()
73+
val parsed = jsonParser.fromJson(peeked, StreamEndpointErrorData::class.java)
7774

78-
// Guard against infinite loops: retry at most once per request.
7975
val alreadyRetried = original.header(HEADER_RETRIED_ON_AUTH) == "present"
8076

8177
if (parsed.isSuccess) {
8278
val error = parsed.getOrEndpointException("Failed to parse error body.")
8379

84-
if (!isTokenInvalidErrorCode(error.code)) {
85-
return firstResponse
86-
}
80+
// Only handle token errors here
81+
if (isTokenInvalidErrorCode(error.code) && !alreadyRetried) {
82+
// refresh & retry once
83+
first.close()
84+
tokenManager.invalidate().getOrEndpointException("Failed to invalidate token")
85+
val refreshed = runBlocking { tokenManager.refresh() }
86+
.getOrEndpointException("Failed to refresh token")
8787

88-
if (!alreadyRetried) {
89-
// Refresh and retry once.
90-
firstResponse.close()
91-
tokenManager
92-
.invalidate()
93-
.getOrEndpointException(message = "Failed to invalidate token")
94-
val refreshed =
95-
runBlocking { tokenManager.refresh() }
96-
.getOrEndpointException("Failed to refresh token")
97-
98-
val retried =
99-
original
100-
.withAuthHeaders(authType, refreshed.rawValue)
101-
.newBuilder()
102-
.header(HEADER_RETRIED_ON_AUTH, "present")
103-
.build()
104-
105-
return chain.proceed(retried)
88+
val retried = original.withAuthHeaders(authType, refreshed.rawValue)
89+
.newBuilder().header(HEADER_RETRIED_ON_AUTH, "present").build()
90+
91+
return chain.proceed(retried) // pass result (ok or error) downstream
10692
}
10793

108-
// Non-token error or we already retried: surface a structured exception.
109-
firstResponse.close()
110-
throw StreamEndpointException("Failed request: ${original.url}", error, null)
94+
// Non-token error, or token error but we already retried:
95+
// pass the original failed response downstream; DO NOT throw here.
96+
return first
11197
} else {
112-
return firstResponse
98+
// Unknown/invalid error body → pass through
99+
return first
113100
}
114101
}
115102

103+
116104
private fun Request.withAuthHeaders(authType: String, bearer: String): Request =
117105
newBuilder()
118106
.addHeader(HEADER_STREAM_AUTH_TYPE, authType)

stream-android-core/src/test/java/io/getstream/android/core/internal/http/interceptor/StreamAuthInterceptorTest.kt

Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import okhttp3.Request
3636
import okhttp3.mockwebserver.MockResponse
3737
import okhttp3.mockwebserver.MockWebServer
3838
import org.junit.After
39+
import org.junit.Assert.assertFalse
40+
import org.junit.Assert.assertNull
3941
import org.junit.Before
4042
import org.junit.Test
4143

@@ -187,109 +189,109 @@ class StreamAuthInterceptorTest {
187189
}
188190

189191
@Test
190-
fun `non-token error throws StreamEndpointException without retry`() {
191-
val token = streamToken("t1")
192+
fun `token error with alreadyRetried header passes through without retry`() {
193+
val token = streamToken("stale")
192194
coEvery { tokenManager.loadIfAbsent() } returns Result.success(token)
193195

194-
val nonTokenError = tokenErrorData(422)
196+
// Proper token error code handled by this interceptor
197+
val tokenError = tokenErrorData(40)
195198
every { json.fromJson(any(), StreamEndpointErrorData::class.java) } returns
196-
Result.success(nonTokenError)
199+
Result.success(tokenError)
197200

198201
val interceptor = StreamAuthInterceptor(tokenManager, json, authType = "jwt")
199202
val client = client(interceptor)
200203

201-
server.enqueue(MockResponse().setResponseCode(422).setBody("""{"error":"unprocessable"}"""))
204+
server.enqueue(MockResponse().setResponseCode(401).setBody("""{"error":"token invalid"}"""))
202205

203-
val url = server.url("/v1/fail")
204-
val ex =
205-
assertFailsWith<StreamEndpointException> {
206-
client
207-
.newCall(Request.Builder().url(url).build())
208-
.execute()
209-
.use { /* force execution */ }
210-
}
211-
assertTrue(ex.message!!.contains("Failed request"))
206+
val url = server.url("/v1/protected")
212207

213-
// Consume the single (failed) request
214-
val first = server.takeRequest(2, TimeUnit.SECONDS)
215-
kotlin.test.assertNotNull(first, "Expected exactly one request to be sent")
208+
client.newCall(
209+
Request.Builder()
210+
.url(url)
211+
.header("x-stream-retried-on-auth", "present") // simulate already retried
212+
.build()
213+
).execute().use { resp ->
214+
assertFalse(resp.isSuccessful) // pass-through, no exception here
215+
assertEquals(401, resp.code)
216+
}
216217

217-
// Assert no second request (i.e., no retry)
218-
val second = server.takeRequest(300, TimeUnit.MILLISECONDS)
219-
kotlin.test.assertNull(second, "Interceptor should not retry on non-token errors")
218+
val first = server.takeRequest(2, TimeUnit.SECONDS)
219+
kotlin.test.assertNotNull(first)
220+
kotlin.test.assertNull(server.takeRequest(300, TimeUnit.MILLISECONDS)) // no second try
220221

222+
// No refresh/invalidate when header indicates we already retried
221223
coVerify(exactly = 1) { tokenManager.loadIfAbsent() }
222-
io.mockk.verify(exactly = 0) { tokenManager.invalidate() }
224+
verify(exactly = 0) { tokenManager.invalidate() }
223225
coVerify(exactly = 0) { tokenManager.refresh() }
224226
}
225227

228+
/**
229+
* Non-token error codes are NOT handled here; pass response through without retry.
230+
*/
226231
@Test
227-
fun `unparseable error throws StreamEndpointException`() {
232+
fun `non-token error passes through without retry`() {
228233
val token = streamToken("t1")
229234
coEvery { tokenManager.loadIfAbsent() } returns Result.success(token)
230235

236+
// e.g., business error code that is not 40/41/42
237+
val nonTokenError = tokenErrorData(13)
231238
every { json.fromJson(any(), StreamEndpointErrorData::class.java) } returns
232-
Result.failure(IllegalStateException("parse error"))
239+
Result.success(nonTokenError)
233240

234241
val interceptor = StreamAuthInterceptor(tokenManager, json, authType = "jwt")
235242
val client = client(interceptor)
236243

237-
server.enqueue(MockResponse().setResponseCode(500).setBody("not-json"))
244+
server.enqueue(MockResponse().setResponseCode(422).setBody("""{"error":"validation"}"""))
238245

239-
val url = server.url("/v1/error")
240-
val ex =
241-
assertFailsWith<StreamEndpointException> {
242-
client.newCall(Request.Builder().url(url).build()).execute().use { /* consume */ }
243-
}
244-
assertTrue(ex.message!!.contains("Failed to serialize response error body"))
246+
val url = server.url("/v1/endpoint")
247+
client.newCall(Request.Builder().url(url).build()).execute().use { resp ->
248+
assertFalse(resp.isSuccessful) // still an error; just passed along
249+
assertEquals(422, resp.code)
250+
}
251+
252+
// No retry, no token refresh
253+
val req = server.takeRequest(2, TimeUnit.SECONDS)!!
254+
assertEquals("t1", req.getHeader("Authorization"))
255+
kotlin.test.assertNull(server.takeRequest(300, TimeUnit.MILLISECONDS))
245256

246-
coVerify(exactly = 1) { tokenManager.loadIfAbsent() }
247257
verify(exactly = 0) { tokenManager.invalidate() }
248258
coVerify(exactly = 0) { tokenManager.refresh() }
249259
}
250260

261+
/**
262+
* If the error body cannot be parsed into StreamEndpointErrorData, pass through.
263+
*/
251264
@Test
252-
fun `token error with alreadyRetried header does not retry again`() {
253-
val token = streamToken("stale")
265+
fun `unparsable error body passes through without retry`() {
266+
val token = streamToken("t1")
254267
coEvery { tokenManager.loadIfAbsent() } returns Result.success(token)
255-
every { tokenManager.invalidate() } returns Result.success(Unit)
256268

257-
val tokenError = tokenErrorData(401)
258269
every { json.fromJson(any(), StreamEndpointErrorData::class.java) } returns
259-
Result.success(tokenError)
270+
Result.failure(IllegalStateException("bad json"))
260271

261272
val interceptor = StreamAuthInterceptor(tokenManager, json, authType = "jwt")
262273
val client = client(interceptor)
263274

264-
server.enqueue(MockResponse().setResponseCode(401).setBody("""{"error":"token invalid"}"""))
275+
server.enqueue(MockResponse().setResponseCode(500).setBody("""<html>oops</html>"""))
265276

266-
val url = server.url("/v1/protected")
267-
val ex =
268-
assertFailsWith<StreamEndpointException> {
269-
client
270-
.newCall(
271-
Request.Builder()
272-
.url(url)
273-
.header(
274-
"x-stream-retried-on-auth",
275-
"present",
276-
) // simulate already retried
277-
.build()
278-
)
279-
.execute()
280-
.use { /* consume */ }
281-
}
282-
assertTrue(ex.message!!.contains("Failed request"))
277+
val url = server.url("/v1/boom")
278+
client.newCall(Request.Builder().url(url).build()).execute().use { resp ->
279+
assertFalse(resp.isSuccessful)
280+
assertEquals(500, resp.code)
281+
}
283282

284-
val first = server.takeRequest(2, TimeUnit.SECONDS)
285-
kotlin.test.assertNotNull(first)
283+
// Consume the single request we expect
284+
val first = server.takeRequest(2, TimeUnit.SECONDS)!!
285+
assertEquals("t1", first.getHeader("Authorization"))
286+
287+
// Now verify there's no retry
286288
kotlin.test.assertNull(server.takeRequest(300, TimeUnit.MILLISECONDS))
287289

288-
coVerify(exactly = 1) { tokenManager.loadIfAbsent() }
289290
verify(exactly = 0) { tokenManager.invalidate() }
290291
coVerify(exactly = 0) { tokenManager.refresh() }
291292
}
292293

294+
293295
// ----------------- Helpers -----------------
294296

295297
private fun client(interceptor: Interceptor): OkHttpClient =

0 commit comments

Comments
 (0)