🍒[PM-33394] fix: Propagate CookieRedirectException error message (#6640)

This commit is contained in:
Patrick Honkonen
2026-03-11 14:37:45 -04:00
committed by GitHub
parent 9bde261007
commit 7f426f1037
3 changed files with 187 additions and 0 deletions

View File

@@ -1,6 +1,9 @@
package com.bitwarden.network.model
import com.bitwarden.network.exception.CookieRedirectException
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
@@ -45,8 +48,26 @@ sealed class BitwardenError {
*/
fun Throwable.toBitwardenError(): BitwardenError {
return when (this) {
// CookieRedirectException is a subclass of IOException thrown when SSO cookies
// expire in a load-balanced environment. It must be checked before IOException to
// avoid being classified as a generic Network error. We synthesize an Http error
// with a JSON body so the exception's message propagates through the existing
// parseErrorBodyOrNull pipeline used by service-layer recoverCatching blocks.
is CookieRedirectException -> {
BitwardenError.Http(
throwable = HttpException(
Response.error<Any>(
HTTP_CODE_BAD_REQUEST,
"""{"message": "${this.message}"}""".toResponseBody(),
),
),
)
}
is IOException -> BitwardenError.Network(this)
is HttpException -> BitwardenError.Http(this)
else -> BitwardenError.Other(this)
}
}
private const val HTTP_CODE_BAD_REQUEST: Int = 400

View File

@@ -0,0 +1,67 @@
package com.bitwarden.network.model
import com.bitwarden.network.exception.CookieRedirectException
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class BitwardenErrorTest {
@Test
fun `toBitwardenError with CookieRedirectException should return Http with status 400`() {
val exception = CookieRedirectException(hostname = "example.com")
val result = exception.toBitwardenError()
assertTrue(result is BitwardenError.Http)
val httpError = result as BitwardenError.Http
assertEquals(400, httpError.code)
}
@Test
fun `toBitwardenError with CookieRedirectException should include message in body`() {
val exception = CookieRedirectException(hostname = "example.com")
val result = exception.toBitwardenError()
val httpError = result as BitwardenError.Http
val body = httpError.responseBodyString
assertTrue(body?.contains(exception.message.orEmpty()) == true)
}
@Test
fun `toBitwardenError with IOException should return Network`() {
val exception = IOException("network failure")
val result = exception.toBitwardenError()
assertTrue(result is BitwardenError.Network)
assertEquals(exception, result.throwable)
}
@Test
fun `toBitwardenError with HttpException should return Http`() {
val exception = HttpException(
Response.error<Unit>(400, "error".toResponseBody()),
)
val result = exception.toBitwardenError()
assertTrue(result is BitwardenError.Http)
assertEquals(exception, result.throwable)
}
@Test
fun `toBitwardenError with RuntimeException should return Other`() {
val exception = RuntimeException("unexpected")
val result = exception.toBitwardenError()
assertTrue(result is BitwardenError.Other)
assertEquals(exception, result.throwable)
}
}

View File

@@ -0,0 +1,99 @@
package com.bitwarden.network.util
import com.bitwarden.network.exception.CookieRedirectException
import com.bitwarden.network.model.BitwardenError
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.toBitwardenError
import kotlinx.serialization.json.Json
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class ExceptionExtensionsTest {
private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
@Test
fun `parseErrorBodyOrNull with CookieRedirectException should extract message`() {
val expectedMessage = "Your request was interrupted because the app " +
"needed to re-authenticate. Please try again."
val error = CookieRedirectException(hostname = "example.com")
.toBitwardenError()
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
codes = listOf(NetworkErrorCode.BAD_REQUEST),
json = json,
)
assertEquals(expectedMessage, result?.message)
}
@Test
fun `parseErrorBodyOrNull with Http and matching code should parse body`() {
val responseBody = """
{
"message": "Bad request",
"validationErrors": {
"Name": ["Name is required"]
}
}
""".trimIndent()
val error = BitwardenError.Http(
throwable = HttpException(
Response.error<Unit>(400, responseBody.toResponseBody()),
),
)
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
codes = listOf(NetworkErrorCode.BAD_REQUEST),
json = json,
)
assertEquals("Bad request", result?.message)
assertEquals(
mapOf("Name" to listOf("Name is required")),
result?.validationErrors,
)
}
@Test
fun `parseErrorBodyOrNull with Http and non-matching code should return null`() {
val responseBody = """
{
"message": "Bad request",
"validationErrors": null
}
""".trimIndent()
val error = BitwardenError.Http(
throwable = HttpException(
Response.error<Unit>(400, responseBody.toResponseBody()),
),
)
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
codes = listOf(NetworkErrorCode.UNAUTHORIZED),
json = json,
)
assertNull(result)
}
@Test
fun `parseErrorBodyOrNull with Network should return null`() {
val error = BitwardenError.Network(throwable = IOException("timeout"))
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
codes = listOf(NetworkErrorCode.BAD_REQUEST),
json = json,
)
assertNull(result)
}
}