From a091549945b0c1604cfe41be9aac6fdfb080719f Mon Sep 17 00:00:00 2001 From: Michael Reed <1062477+haywood@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:43:52 -0500 Subject: [PATCH] fix: Improve error handling in Transport.kt (#1540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Provide HTTP request and response details on SDKErrorResponse. - Provide the cause of SDKError for chaining and re-throwing - Add LookerApiException, which includes the same request/response information as SDKErrorResponse. - Mark ok() as Deprecated, because it throws java.lang.Error, which is not something application code should do. - Add SDKResponse::getOrThrow() instance method, patterned after Kotlin's built-in Result type, which handles SDKErrorResponse by throwing the newly introduced LookerApiException Fixes #1539 🦕 --- kotlin/src/main/com/looker/rtl/Transport.kt | 64 +++++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/kotlin/src/main/com/looker/rtl/Transport.kt b/kotlin/src/main/com/looker/rtl/Transport.kt index 51e8a6d07..2806e67d3 100644 --- a/kotlin/src/main/com/looker/rtl/Transport.kt +++ b/kotlin/src/main/com/looker/rtl/Transport.kt @@ -73,23 +73,67 @@ sealed class SDKResponse { data class SDKErrorResponse( /** The error object returned by the SDK call. */ val value: T, + val method: HttpMethod, + val path: String, + val statusCode: Int, + val statusMessage: String, + val responseHeaders: HttpHeaders, + val responseBody: String, ) : SDKResponse() { /** Whether the SDK call was successful. */ val ok: Boolean = false } /** An error representing an issue in the SDK, like a network or parsing error. */ - data class SDKError(val message: String) : SDKResponse() { + data class SDKError(val message: String, val cause: Exception) : SDKResponse() { val type: String = "sdk_error" } + + inline fun getOrThrow(): V = + when (this) { + is SDKResponse.SDKSuccessResponse<*> -> + checkNotNull(value as? V) { + if (value == null) { + "Expected value of type ${V::class}, but was null" + } else { + "Expected value of type ${V::class}, but was ${value::class}" + } + } + + is SDKResponse.SDKErrorResponse<*> -> + throw LookerApiException( + method, + path, + statusCode, + statusMessage, + responseHeaders, + responseBody, + ) + + is SDKResponse.SDKError -> throw cause + } + companion object { const val ERROR_BODY = "error_body" } } -/** - * Response handler that throws an error on error response, returns success result on success - */ +/** Thrown when a Looker API call returns an error. */ +data class LookerApiException( + val method: HttpMethod, + val path: String, + val statusCode: Int, + val statusMessage: String, + val responseHeaders: HttpHeaders, + val responseBody: String, +) : Exception() { + override val message = "$method $path $statusCode: $statusMessage" +} + +/** Response handler that throws an error on error response, returns success result on success */ +@Deprecated( + "This method throws java.lang.Error, which is not recommended for use in application code. Please use SDKResponse.getOrThrow() instead." +) fun ok(response: SDKResponse): T { @Suppress("UNCHECKED_CAST") when (response) { @@ -379,9 +423,17 @@ open class Transport(val options: TransportOptions) { } SDKResponse.SDKSuccessResponse(rawResult) } catch (e: HttpResponseException) { - SDKResponse.SDKErrorResponse("$method $path $ERROR_BODY: ${e.content}") + SDKResponse.SDKErrorResponse( + "$method $path $ERROR_BODY: ${e.content}", + method, + path, + e.statusCode, + e.statusMessage, + e.headers, + e.content, + ) } catch (e: Exception) { - SDKResponse.SDKError(e.message ?: "Something went wrong") + SDKResponse.SDKError(e.message ?: "Something went wrong", e) } return sdkResponse