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

fix: commit bundles special api response handling (WPB-2229) #2296

Merged
merged 4 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol
import io.ktor.http.Url
import io.ktor.http.isSuccess
import io.ktor.serialization.JsonConvertException

internal fun HttpRequestBuilder.setWSSUrl(baseUrl: Url, vararg path: String) {
url {
Expand Down Expand Up @@ -237,23 +238,19 @@ private fun toStatusCodeBasedKaliumException(

/**
* Wrap and handles federation aware endpoints that can send errors responses
* And raise proper federated exceptions
* And raise specific federated context exceptions,
*
* @delegatedHandler the fallback handler when the response is not a federation error
* i.e. FederationError, FederationUnreachableException, FederationConflictException
*
* @param response the response to wrap
* @param delegatedHandler the fallback handler when the response cannot be handled as a federation error
*/
suspend fun <T : Any> wrapFederationResponse(
response: HttpResponse,
delegatedHandler: suspend (HttpResponse) -> NetworkResponse<T>
) =
when (response.status.value) {
HttpStatusCode.Conflict.value -> {
val errorResponse = try {
response.body()
} catch (_: NoTransformationFoundException) {
FederationConflictResponse(emptyList())
}
NetworkResponse.Error(KaliumException.FederationConflictException(errorResponse))
}
HttpStatusCode.Conflict.value -> resolveStatusCodeBasedFirstOrFederated(response)

HttpStatusCode.UnprocessableEntity.value -> {
val errorResponse = try {
Expand All @@ -277,3 +274,27 @@ suspend fun <T : Any> wrapFederationResponse(
delegatedHandler.invoke(response)
}
}

/**
* Due to the "shared" status code limitations nature of some endpoints.
* We need to first delegate to status code based exceptions and if parse fails go for federated error,
*
* i.e.: '/commit-bundles' 409 for "mls-stale-message" and 409 for "federation-conflict"
*/
private suspend fun resolveStatusCodeBasedFirstOrFederated(response: HttpResponse): NetworkResponse.Error {
val kaliumException = try {
val errorResponse = response.body<ErrorResponse>()
toStatusCodeBasedKaliumException(
response.status,
response,
errorResponse
)
} catch (exception: JsonConvertException) {
try {
KaliumException.FederationConflictException(response.body<FederationConflictResponse>())
} catch (_: NoTransformationFoundException) {
KaliumException.FederationConflictException(FederationConflictResponse(emptyList()))
}
}
return NetworkResponse.Error(kaliumException)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@
package com.wire.kalium.api.v5

import com.wire.kalium.api.ApiTest
import com.wire.kalium.api.json.model.ErrorResponseJson
import com.wire.kalium.model.EventContentDTOJson
import com.wire.kalium.model.SendMLSMessageResponseJson
import com.wire.kalium.network.api.base.authenticated.message.MLSMessageApi
import com.wire.kalium.network.api.base.model.ErrorResponse
import com.wire.kalium.network.api.base.model.FederationConflictResponse
import com.wire.kalium.network.api.v0.authenticated.MLSMessageApiV0
import com.wire.kalium.network.api.v5.authenticated.MLSMessageApiV5
import com.wire.kalium.network.exceptions.KaliumException
import com.wire.kalium.network.serialization.Mls
import com.wire.kalium.network.utils.UnreachableRemoteBackends
import com.wire.kalium.network.utils.isSuccessful
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

Expand Down Expand Up @@ -80,6 +87,76 @@ internal class MLSMessageApiV5Test : ApiTest() {
assertFalse(response.isSuccessful())
}

@Test
fun givenCommitBundle_whenSendingBundleFailsUnreachable_theRequestShouldFailWithUnreachableError() = runTest {
val networkClient = mockAuthenticatedNetworkClient(
EventContentDTOJson.jsonProviderMemberJoinFailureUnreachable,
statusCode = HttpStatusCode.UnreachableRemoteBackends,
assertion =
{
assertPost()
assertContentType(ContentType.Message.Mls)
assertPathEqual(PATH_COMMIT_BUNDLES)
}
)
val mlsMessageApi: MLSMessageApi = MLSMessageApiV5(networkClient)
val response = mlsMessageApi.sendCommitBundle(COMMIT_BUNDLE)

assertFalse(response.isSuccessful())
assertEquals(KaliumException.FederationUnreachableException::class, response.kException::class)
}

@Test
fun givenCommitBundle_whenSendingBundleFailsConflict_theRequestShouldFailWithConflictDomainsError() = runTest {
val nonFederatingBackends = listOf("bella.wire.link", "foma.wire.link")
val networkClient = mockAuthenticatedNetworkClient(
ErrorResponseJson.validFederationConflictingBackends(
FederationConflictResponse(nonFederatingBackends)
).rawJson,
statusCode = HttpStatusCode.Conflict,
assertion =
{
assertPost()
assertContentType(ContentType.Message.Mls)
assertPathEqual(PATH_COMMIT_BUNDLES)
}
)
val mlsMessageApi: MLSMessageApi = MLSMessageApiV5(networkClient)
val response = mlsMessageApi.sendCommitBundle(COMMIT_BUNDLE)

assertFalse(response.isSuccessful())
assertEquals(KaliumException.FederationConflictException::class, response.kException::class)
assertEquals(
expected = nonFederatingBackends,
actual = (response.kException as KaliumException.FederationConflictException).errorResponse.nonFederatingBackends
)
}

@Test
fun givenCommitBundle_whenSendingBundleFailsConflictNonFederated_theRequestShouldFailWithRegularInvalidRequestError() = runTest {
val networkClient = mockAuthenticatedNetworkClient(
ErrorResponseJson.valid(
ErrorResponse(
code = HttpStatusCode.Conflict.value,
message = "some other error",
label = "mls-stale-message"
)
).rawJson,
statusCode = HttpStatusCode.Conflict,
assertion =
{
assertPost()
assertContentType(ContentType.Message.Mls)
assertPathEqual(PATH_COMMIT_BUNDLES)
}
)
val mlsMessageApi: MLSMessageApi = MLSMessageApiV5(networkClient)
val response = mlsMessageApi.sendCommitBundle(COMMIT_BUNDLE)

assertFalse(response.isSuccessful())
assertEquals(KaliumException.InvalidRequestError::class, response.kException::class)
}

private companion object {
const val PATH_MESSAGE = "/mls/messages"
const val PATH_COMMIT_BUNDLES = "mls/commit-bundles"
Expand Down
Loading