我正在寻找更好的方法来处理错误和异常。
我想捕获异常并将其传递回该函数的调用者。但方法签名是
<LoginResponseDto, ErrorResponseDto>
ErrorResponseDto 是当 API 返回状态代码错误时我将返回的模型。但是,可能会引发异常,我也想将其冒泡给调用者。
我不确定我当前的实现是否可行。
处理此类事情的最佳方法是什么
override suspend fun loginUser(loginRequestModel: LoginRequestModel): APIResponse<LoginResponseDto, ErrorResponseDto> {
return try {
val response = httpClient
.post("https://endpoint") {
contentType(ContentType.Application.Json)
setBody(
LoginRequestDto(
/* body data */
)
)
}
if (response.status.value == 200) {
APIResponse.OnSuccess(response.body())
}
else {
APIResponse.OnFailure(response.body())
}
}
catch (exception: Exception) {
if (exception is CancellationException) {
Timber.e(exception)
throw exception
}
else {
Timber.e(exception)
// This works as I am still return the ErrorResponseDto but looks hacky doing it like this
APIResponse.OnFailure(ErrorResponseDto(
errors = listOf(
ErrorDto(
code = exception.localizedMessage ?: "",
detail = exception.message ?: "Unknown"))))
// But what I would want to do is this
// APIResponse.OnFailure(exception)
}
}
}
interface APIResponse<out T, out E> {
data class OnSuccess<T>(val data: T) : APIResponse<T, Nothing>
data class OnFailure<E>(val error: E) : APIResponse<Nothing, E>
}
data class ErrorResponseDto(
val errors: List<ErrorDto>
)
data class ErrorDto(
val code: String,
val detail: String
)
如果您有构造
Success
和 Failure
实例的方法以及可以接受异常并返回 APIResponse<Nothing, ErrorResponseDto>
的重载方法,该怎么办?
interface APIResponse<out T, out E> {
data class Success<T>(val data: T) : APIResponse<T, Nothing>
data class Failure<E>(val error: E) : APIResponse<Nothing, E>
companion object {
fun <T> onSuccess(data: T) = Success(data)
fun <E> onFailure(error: E) = Failure(error)
fun onFailure(cause: Exception): APIResponse<Nothing, ErrorResponseDto> {
return Failure(ErrorResponseDto(
errors = listOf(
ErrorDto(
code = cause.localizedMessage ?: "",
detail = cause.message ?: "Unknown"))))
}
}
}
这样,您可以使用
APIResponse.OnFailure(exception)
来简化处理 HTTP 响应的代码。
我个人将客户端设置为期望成功,然后使用封装所有错误的
kotlin.Result
而不是返回奇怪的类型接口。
唯一要记住的是,当 http 调用本身是失败的原因时,Ktor 会给我们
ResponseException
。
override suspend fun loginUser(loginRequestModel: LoginRequestModel): Result<LoginResponseDto> =
runCatching {
// note that client is set up to expect success so it will throw when response code is not 200
httpClient
.post("https://endpoint") {
contentType(ContentType.Application.Json)
setBody(LoginRequestDto(/* body data */))
}.body()
}.onFailure {
// bubble up cancellation
if (it is CancellationException) throw it
}
现在在呼叫站点中剩下的就是:
api.loginUser(loginModel).fold (
onSuccess = { loginDto -> handleSuccessfullLogin(loginDto) },
onFailure = { error ->
when (error) {
is ResponseException -> { // http call failed here
error.response.status // returned code and message is here
}
else -> { /* other causes */ }
}
}
)
ResponseException
如何包含完整的HttpResonse
,这意味着如果需要的话我们也可以尝试获取错误.body()
。
我的一些偏好,基于关注点分离,这可能会给你一些关于你自己的解决方案的想法。
消费观点
视图通过指示其视图模型调用 API 来发起处理。出现错误时,消费视图可能会显示子错误视图。
“错误摘要”视图可能会显示基本消息。单击它时,您可能会看到一个错误详细信息模式对话框,它会呈现您的代码和详细信息,以及可能的其他详细信息。
消费视图模型视图模型是捕获 API 错误并填充错误视图模型的地方。但这不是您实施管道的地方。相反,目标是只编写以业务为中心的代码,如下所示:
try {
val result = loginClient.loginUser(request)
withContext(Dispatchers.Main) {
updateData(result)
}
} catch (error: AppError) {
withContext(Dispatchers.Main) {
updateError(error)
}
}
对于每个远程 API(例如登录服务),使用专用类与其交互,该类实现任何管道,例如从 API 错误响应转换为应用程序的错误视图模型。
可扩展性我对前端应用程序的目标通常是使用简单的代码构建许多视图和视图模型,而管道代码(例如涉及远程调用的代码)是外部化的。我认为上述模式满足这些要求。
总结在你的情况下,我会让你的
loginUser
逻辑成为服务代理方法。它的返回类型应该是
LoginResponseDto
。失败时,loginUser
会抛出一个错误,这对于视图模型来说很容易使用。例如,它可能会从
ErrorResponseDto
翻译为 AppError
。sealed class APIErrorException(message: String, cause: Throwable? = null) : Exception(message, cause) {
class InvalidCredentialsException(message: String) : APIErrorException(message)
class AccessDeniedException(message: String) : APIErrorException(message)
class ResourceNotFoundException(message: String) : APIErrorException(message)
class UnknownAPIException(message: String) : APIErrorException(message)
}
然后您可以更新 APIResponse 接口并在代码中使用,如下所示:
sealed interface APIResponse<out T, out E : Exception> {
data class Success<T>(val data: T) : APIResponse<T, Nothing>
data class Failure<E>(val error: E) : APIResponse<Nothing, E>
data class Error(val exception: Exception) : APIResponse<Nothing, Nothing>
}
override suspend fun loginUser(loginRequestModel: LoginRequestModel): APIResponse<LoginResponseDto, Exception> {
return try {
val response = httpClient.post("https://endpoint") {
contentType(ContentType.Application.Json)
setBody(LoginRequestDto(/* body data */))
}
when (response.status.value) {
200 -> APIResponse.Success(response.body())
401 -> throw APIErrorException.InvalidCredentialsException("Invalid credentials provided")
403 -> throw APIErrorException.AccessDeniedException("Access denied")
404 -> throw APIErrorException.ResourceNotFoundException("Resource not found")
else -> APIResponse.Failure(ErrorResponseDto(errors = listOf(ErrorDto("APIError", "Unknown error"))))
}
} catch (e: CancellationException) {
Timber.e(e)
//Maybe rethrow ?
} catch (e: Exception) {
Timber.e(e)
APIResponse.Error(e)
}
}