From 7b7c746352c83bed44cb4659f6ba5566750c3e5b Mon Sep 17 00:00:00 2001 From: Justin Brooks Date: Tue, 3 Oct 2023 13:32:30 -0400 Subject: [PATCH] Rework ConnectError to ConnectException (#120) --- .../com/connectrpc/conformance/Conformance.kt | 61 ++++++++++--------- .../com/connectrpc/examples/kotlin/Main.kt | 10 +-- .../com/connectrpc/examples/kotlin/Main.kt | 10 +-- .../{ConnectError.kt => ConnectException.kt} | 10 +-- .../kotlin/com/connectrpc/ResponseMessage.kt | 14 ++--- .../kotlin/com/connectrpc/StreamResult.kt | 15 ++--- .../com/connectrpc/http/HTTPResponse.kt | 4 +- .../com/connectrpc/impl/ClientOnlyStream.kt | 16 ++--- .../com/connectrpc/impl/ProtocolClient.kt | 10 +-- .../protocols/ConnectInterceptor.kt | 24 ++++---- .../connectrpc/protocols/GRPCCompletion.kt | 40 ++++++------ .../connectrpc/protocols/GRPCInterceptor.kt | 44 +++++++------ .../protocols/GRPCWebInterceptor.kt | 16 +++-- ...ctErrorTest.kt => ConnectExceptionTest.kt} | 10 +-- .../com/connectrpc/InterceptorChainTest.kt | 2 +- .../protocols/ConnectInterceptorTest.kt | 18 +++--- .../protocols/GRPCInterceptorTest.kt | 10 +-- .../protocols/GRPCWebInterceptorTest.kt | 12 ++-- .../connectrpc/okhttp/ConnectOkHttpClient.kt | 6 +- .../com/connectrpc/okhttp/OkHttpStream.kt | 12 ++-- 20 files changed, 175 insertions(+), 169 deletions(-) rename library/src/main/kotlin/com/connectrpc/{ConnectError.kt => ConnectException.kt} (91%) rename library/src/test/kotlin/com/connectrpc/{ConnectErrorTest.kt => ConnectExceptionTest.kt} (86%) diff --git a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt b/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt index 19a43b7d..332cbcd6 100644 --- a/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt +++ b/conformance/google-java/src/test/kotlin/com/connectrpc/conformance/Conformance.kt @@ -15,7 +15,7 @@ package com.connectrpc.conformance import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.ProtocolClientConfig import com.connectrpc.RequestCompression @@ -44,6 +44,7 @@ import com.google.protobuf.empty import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -177,7 +178,7 @@ class Conformance( }, ).getOrThrow() val results = streamResults(stream.resultChannel()) - assertThat(results.error).isNull() + assertThat(results.cause).isNull() assertThat(results.code).isEqualTo(Code.OK) assertThat(results.messages.map { it.payload.type }.toSet()).isEqualTo(setOf(PayloadType.COMPRESSABLE)) assertThat(results.messages.map { it.payload.body.size() }).isEqualTo(sizes) @@ -211,7 +212,7 @@ class Conformance( val results = streamResults(stream.resultChannel()) // We've already read all the messages assertThat(results.messages).isEmpty() - assertThat(results.error).isNull() + assertThat(results.cause).isNull() assertThat(results.code).isEqualTo(Code.OK) stream.receiveClose() } @@ -247,11 +248,11 @@ class Conformance( val result = streamResults(stream.resultChannel()) assertThat(result.messages.map { it.payload.body.size() }).isEqualTo(sizes) assertThat(result.code).isEqualTo(Code.RESOURCE_EXHAUSTED) - assertThat(result.error).isInstanceOf(ConnectError::class.java) - val connectError = result.error as ConnectError - assertThat(connectError.code).isEqualTo(Code.RESOURCE_EXHAUSTED) - assertThat(connectError.message).isEqualTo("soirée 🎉") - assertThat(connectError.unpackedDetails(ErrorDetail::class)).containsExactly( + assertThat(result.cause).isInstanceOf(ConnectException::class.java) + val connectException = result.cause as ConnectException + assertThat(connectException.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(connectException.message).isEqualTo("soirée 🎉") + assertThat(connectException.unpackedDetails(ErrorDetail::class)).containsExactly( expectedErrorDetail, ) } finally { @@ -329,9 +330,9 @@ class Conformance( testServiceConnectClient.unaryCall(message) { response -> assertThat(response.code).isEqualTo(Code.UNKNOWN) response.failure { errorResponse -> - assertThat(errorResponse.error).isNotNull() + assertThat(errorResponse.cause).isNotNull() assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN) - assertThat(errorResponse.error.message).isEqualTo("test status message") + assertThat(errorResponse.cause.message).isEqualTo("test status message") countDownLatch.countDown() } response.success { @@ -360,13 +361,13 @@ class Conformance( } val stream = client.streamingOutputCall() withContext(Dispatchers.IO) { - val job = async { + val job = launch { try { val result = streamResults(stream.resultChannel()) - assertThat(result.error).isInstanceOf(ConnectError::class.java) - val connectErr = result.error as ConnectError - assertThat(connectErr.code).isEqualTo(Code.DEADLINE_EXCEEDED) - assertThat(result.code).isEqualTo(Code.DEADLINE_EXCEEDED) + assertThat(result.cause).isInstanceOf(ConnectException::class.java) + assertThat(result.code) + .withFailMessage { "Expected Code.DEADLINE_EXCEEDED but got ${result.code}" } + .isEqualTo(Code.DEADLINE_EXCEEDED) } finally { countDownLatch.countDown() } @@ -392,7 +393,7 @@ class Conformance( }, ) { response -> response.failure { errorResponse -> - val error = errorResponse.error + val error = errorResponse.cause assertThat(error.code).isEqualTo(Code.UNKNOWN) assertThat(response.code).isEqualTo(Code.UNKNOWN) assertThat(error.message).isEqualTo(statusMessage) @@ -438,9 +439,9 @@ class Conformance( try { val result = streamResults(stream.resultChannel()) assertThat(result.code).isEqualTo(Code.UNIMPLEMENTED) - assertThat(result.error).isInstanceOf(ConnectError::class.java) - val connectErr = result.error as ConnectError - assertThat(connectErr.code).isEqualTo(Code.UNIMPLEMENTED) + assertThat(result.cause).isInstanceOf(ConnectException::class.java) + val exception = result.cause as ConnectException + assertThat(exception.code).isEqualTo(Code.UNIMPLEMENTED) } finally { countDownLatch.countDown() } @@ -461,7 +462,7 @@ class Conformance( testServiceConnectClient.failUnaryCall(simpleRequest {}) { response -> assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED) response.failure { errorResponse -> - val error = errorResponse.error + val error = errorResponse.cause assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED) assertThat(error.message).isEqualTo("soirée 🎉") val connectErrorDetails = error.unpackedDetails(ErrorDetail::class) @@ -544,9 +545,9 @@ class Conformance( val response = testServiceConnectClient.unaryCallBlocking(message).execute() assertThat(response.code).isEqualTo(Code.UNKNOWN) response.failure { errorResponse -> - assertThat(errorResponse.error).isNotNull() + assertThat(errorResponse.cause).isNotNull() assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN) - assertThat(errorResponse.error.message).isEqualTo("test status message") + assertThat(errorResponse.cause.message).isEqualTo("test status message") } response.success { fail("unexpected success") @@ -566,7 +567,7 @@ class Conformance( }, ).execute() response.failure { errorResponse -> - val error = errorResponse.error + val error = errorResponse.cause assertThat(error.code).isEqualTo(Code.UNKNOWN) assertThat(response.code).isEqualTo(Code.UNKNOWN) assertThat(error.message).isEqualTo(statusMessage) @@ -597,7 +598,7 @@ class Conformance( val response = testServiceConnectClient.failUnaryCallBlocking(simpleRequest {}).execute() assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED) response.failure { errorResponse -> - val error = errorResponse.error + val error = errorResponse.cause assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED) assertThat(error.message).isEqualTo("soirée 🎉") val connectErrorDetails = error.unpackedDetails(ErrorDetail::class) @@ -692,9 +693,9 @@ class Conformance( testServiceConnectClient.unaryCall(message) { response -> assertThat(response.code).isEqualTo(Code.UNKNOWN) response.failure { errorResponse -> - assertThat(errorResponse.error).isNotNull() + assertThat(errorResponse.cause).isNotNull() assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN) - assertThat(errorResponse.error.message).isEqualTo("test status message") + assertThat(errorResponse.cause.message).isEqualTo("test status message") countDownLatch.countDown() } response.success { @@ -720,7 +721,7 @@ class Conformance( }, ) { response -> response.failure { errorResponse -> - val error = errorResponse.error + val error = errorResponse.cause assertThat(error.code).isEqualTo(Code.UNKNOWN) assertThat(response.code).isEqualTo(Code.UNKNOWN) assertThat(error.message).isEqualTo(statusMessage) @@ -766,7 +767,7 @@ class Conformance( testServiceConnectClient.failUnaryCall(simpleRequest {}) { response -> assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED) response.failure { errorResponse -> - val error = errorResponse.error + val error = errorResponse.cause assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED) assertThat(error.message).isEqualTo("soirée 🎉") val connectErrorDetails = error.unpackedDetails(ErrorDetail::class) @@ -817,7 +818,7 @@ class Conformance( val messages: List, val code: Code, val trailers: Trailers, - val error: Throwable?, + val cause: Throwable?, ) /* @@ -852,7 +853,7 @@ class Conformance( } code = it.code trailers = it.trailers - error = it.error + error = it.cause }, ) } diff --git a/examples/kotlin-google-java/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt b/examples/kotlin-google-java/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt index bd611d87..26a994c0 100644 --- a/examples/kotlin-google-java/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt +++ b/examples/kotlin-google-java/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt @@ -15,7 +15,7 @@ package com.connectrpc.examples.kotlin import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.ProtocolClientConfig import com.connectrpc.eliza.v1.ElizaServiceClient import com.connectrpc.eliza.v1.converseRequest @@ -72,11 +72,11 @@ class Main { }, onCompletion = { result -> if (result.code != Code.OK) { - val connectErr = result.connectError() - if (connectErr != null) { - throw connectErr + val exception = result.connectException() + if (exception != null) { + throw exception } - throw ConnectError(code = result.code, metadata = result.trailers) + throw ConnectException(code = result.code, metadata = result.trailers) } }, ) diff --git a/examples/kotlin-google-javalite/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt b/examples/kotlin-google-javalite/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt index 4068404b..bd3b187b 100644 --- a/examples/kotlin-google-javalite/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt +++ b/examples/kotlin-google-javalite/src/main/kotlin/com/connectrpc/examples/kotlin/Main.kt @@ -15,7 +15,7 @@ package com.connectrpc.examples.kotlin import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.ProtocolClientConfig import com.connectrpc.eliza.v1.ElizaServiceClient import com.connectrpc.eliza.v1.converseRequest @@ -72,11 +72,11 @@ class Main { }, onCompletion = { result -> if (result.code != Code.OK) { - val connectErr = result.connectError() - if (connectErr != null) { - throw connectErr + val exception = result.connectException() + if (exception != null) { + throw exception } - throw ConnectError(code = result.code, metadata = result.trailers) + throw ConnectException(code = result.code, metadata = result.trailers) } }, ) diff --git a/library/src/main/kotlin/com/connectrpc/ConnectError.kt b/library/src/main/kotlin/com/connectrpc/ConnectException.kt similarity index 91% rename from library/src/main/kotlin/com/connectrpc/ConnectError.kt rename to library/src/main/kotlin/com/connectrpc/ConnectException.kt index 729913df..fe7a48dd 100644 --- a/library/src/main/kotlin/com/connectrpc/ConnectError.kt +++ b/library/src/main/kotlin/com/connectrpc/ConnectException.kt @@ -20,7 +20,7 @@ import kotlin.reflect.KClass * Typed error provided by Connect RPCs that may optionally wrap additional typed custom errors * using [details]. */ -data class ConnectError( +data class ConnectException( // The resulting status code. val code: Code, private val errorDetailParser: ErrorDetailParser? = null, @@ -32,7 +32,7 @@ data class ConnectError( val details: List = emptyList(), // Additional key-values that were provided by the server. val metadata: Headers = emptyMap(), -) : Throwable(message, exception) { +) : Exception(message, exception) { /** * Unpacks values from [details] and returns the first matching error, if any. @@ -51,10 +51,10 @@ data class ConnectError( } /** - * Creates a new [ConnectError] with the specified [ErrorDetailParser]. + * Creates a new [ConnectException] with the specified [ErrorDetailParser]. */ - fun setErrorParser(errorParser: ErrorDetailParser): ConnectError { - return ConnectError( + fun setErrorParser(errorParser: ErrorDetailParser): ConnectException { + return ConnectException( code, errorParser, message, diff --git a/library/src/main/kotlin/com/connectrpc/ResponseMessage.kt b/library/src/main/kotlin/com/connectrpc/ResponseMessage.kt index 1d96b959..4eeb992d 100644 --- a/library/src/main/kotlin/com/connectrpc/ResponseMessage.kt +++ b/library/src/main/kotlin/com/connectrpc/ResponseMessage.kt @@ -41,8 +41,8 @@ sealed class ResponseMessage( } class Failure( - // The error. - val error: ConnectError, + // The problem. + val cause: ConnectException, // The status code of the response. override val code: Code, // Response headers specified by the server. @@ -51,7 +51,7 @@ sealed class ResponseMessage( override val trailers: Trailers, ) : ResponseMessage(code, headers, trailers) { override fun toString(): String { - return "Failure{error=$error,code=$code,headers=$headers,trailers=$trailers}" + return "Failure{cause=$cause,code=$code,headers=$headers,trailers=$trailers}" } } @@ -77,7 +77,7 @@ sealed class ResponseMessage( fun ResponseMessage<*>.exceptionOrNull(): Throwable? { return when (this) { is ResponseMessage.Success -> null - is ResponseMessage.Failure -> this.error + is ResponseMessage.Failure -> this.cause } } @@ -93,7 +93,7 @@ inline fun ResponseMessage.fold( ): R { return when (this) { is ResponseMessage.Success -> onSuccess(this.message) - is ResponseMessage.Failure -> onFailure(this.error) + is ResponseMessage.Failure -> onFailure(this.cause) } } @@ -117,7 +117,7 @@ fun ResponseMessage.getOrDefault(defaultValue: T): T { inline fun ResponseMessage.getOrElse(onFailure: (exception: Throwable) -> R): R { return when (this) { is ResponseMessage.Success -> this.message - is ResponseMessage.Failure -> onFailure(this.error) + is ResponseMessage.Failure -> onFailure(this.cause) } } @@ -139,6 +139,6 @@ fun ResponseMessage.getOrNull(): T? { fun ResponseMessage.getOrThrow(): T { return when (this) { is ResponseMessage.Success -> this.message - is ResponseMessage.Failure -> throw this.error + is ResponseMessage.Failure -> throw this.cause } } diff --git a/library/src/main/kotlin/com/connectrpc/StreamResult.kt b/library/src/main/kotlin/com/connectrpc/StreamResult.kt index 46ab6082..46e721b3 100644 --- a/library/src/main/kotlin/com/connectrpc/StreamResult.kt +++ b/library/src/main/kotlin/com/connectrpc/StreamResult.kt @@ -35,21 +35,16 @@ sealed class StreamResult { } // Stream is complete. Provides the end status code and optionally an error and trailers. - class Complete(val code: Code, val error: Throwable? = null, val trailers: Trailers = emptyMap()) : StreamResult() { + class Complete(val code: Code, val cause: Throwable? = null, val trailers: Trailers = emptyMap()) : StreamResult() { /** - * Get the ConnectError from the result. + * Get the ConnectException from the result. * - * @return The [ConnectError] if present, null otherwise. + * @return The [ConnectException] if present, null otherwise. */ - fun connectError(): ConnectError? { - if (error is ConnectError) { - return error - } - return null - } + fun connectException() = cause as? ConnectException override fun toString(): String { - return "Complete{code=$code,error=$error,trailers=$trailers}" + return "Complete{code=$code,cause=$cause,trailers=$trailers}" } } diff --git a/library/src/main/kotlin/com/connectrpc/http/HTTPResponse.kt b/library/src/main/kotlin/com/connectrpc/http/HTTPResponse.kt index a58076f4..31d0c153 100644 --- a/library/src/main/kotlin/com/connectrpc/http/HTTPResponse.kt +++ b/library/src/main/kotlin/com/connectrpc/http/HTTPResponse.kt @@ -15,7 +15,7 @@ package com.connectrpc.http import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.Trailers import okio.BufferedSource @@ -38,5 +38,5 @@ class HTTPResponse( // null in cases where no response was received from the server. val tracingInfo: TracingInfo?, // The accompanying error, if the request failed. - val error: ConnectError? = null, + val cause: ConnectException? = null, ) diff --git a/library/src/main/kotlin/com/connectrpc/impl/ClientOnlyStream.kt b/library/src/main/kotlin/com/connectrpc/impl/ClientOnlyStream.kt index 398278de..e449c7f0 100644 --- a/library/src/main/kotlin/com/connectrpc/impl/ClientOnlyStream.kt +++ b/library/src/main/kotlin/com/connectrpc/impl/ClientOnlyStream.kt @@ -17,7 +17,7 @@ package com.connectrpc.impl import com.connectrpc.BidirectionalStreamInterface import com.connectrpc.ClientOnlyStreamInterface import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.ResponseMessage @@ -40,12 +40,12 @@ internal class ClientOnlyStream( // in the channel (headers, 1* messages, completion) and have to resort to fold()/maybeFold() to interpret // the overall results. // Additionally, ResponseMessage.Success and ResponseMessage.Failure shouldn't be necessary for client use. - // We should throw ConnectError for failure and only have users have to deal with success messages. + // We should throw ConnectException for failure and only have users have to deal with success messages. var headers: Headers = emptyMap() var message: Output? = null var trailers: Headers = emptyMap() var code: Code? = null - var error: ConnectError? = null + var error: ConnectException? = null for (result in resultChannel) { result.maybeFold( onHeaders = { @@ -57,12 +57,12 @@ internal class ClientOnlyStream( onCompletion = { code = it.code trailers = it.trailers - val resultErr = it.error + val resultErr = it.cause if (resultErr != null) { - error = if (resultErr is ConnectError) { + error = if (resultErr is ConnectException) { resultErr } else { - ConnectError(code ?: Code.UNKNOWN, message = error?.message, exception = error, metadata = trailers) + ConnectException(code ?: Code.UNKNOWN, message = error?.message, exception = error, metadata = trailers) } } }, @@ -72,14 +72,14 @@ internal class ClientOnlyStream( return ResponseMessage.Failure(error!!, code ?: Code.UNKNOWN, headers, trailers) } if (code == null) { - return ResponseMessage.Failure(ConnectError(Code.UNKNOWN, message = "unknown status code"), Code.UNKNOWN, headers, trailers) + return ResponseMessage.Failure(ConnectException(Code.UNKNOWN, message = "unknown status code"), Code.UNKNOWN, headers, trailers) } if (message != null) { return ResponseMessage.Success(message!!, code!!, headers, trailers) } // We didn't receive an error at any point, however we didn't get a response message either. return ResponseMessage.Failure( - ConnectError(Code.UNKNOWN, message = "missing response message"), + ConnectException(Code.UNKNOWN, message = "missing response message"), code!!, headers, trailers, diff --git a/library/src/main/kotlin/com/connectrpc/impl/ProtocolClient.kt b/library/src/main/kotlin/com/connectrpc/impl/ProtocolClient.kt index 6fafeb38..56548a3b 100644 --- a/library/src/main/kotlin/com/connectrpc/impl/ProtocolClient.kt +++ b/library/src/main/kotlin/com/connectrpc/impl/ProtocolClient.kt @@ -73,11 +73,11 @@ class ProtocolClient( val cancelable = httpClient.unary(finalRequest) { httpResponse -> val finalResponse = unaryFunc.responseFunction(httpResponse) val code = finalResponse.code - val connectError = finalResponse.error?.setErrorParser(serializationStrategy.errorDetailParser()) - if (connectError != null) { + val exception = finalResponse.cause?.setErrorParser(serializationStrategy.errorDetailParser()) + if (exception != null) { onResult( ResponseMessage.Failure( - connectError, + exception, code, finalResponse.headers, finalResponse.trailers, @@ -204,8 +204,8 @@ class ProtocolClient( is StreamResult.Complete -> { isComplete = true StreamResult.Complete( - streamResult.connectError()?.code ?: Code.OK, - error = streamResult.error, + streamResult.connectException()?.code ?: Code.OK, + cause = streamResult.cause, trailers = streamResult.trailers, ) } diff --git a/library/src/main/kotlin/com/connectrpc/protocols/ConnectInterceptor.kt b/library/src/main/kotlin/com/connectrpc/protocols/ConnectInterceptor.kt index bf625a3a..215986bf 100644 --- a/library/src/main/kotlin/com/connectrpc/protocols/ConnectInterceptor.kt +++ b/library/src/main/kotlin/com/connectrpc/protocols/ConnectInterceptor.kt @@ -16,8 +16,8 @@ package com.connectrpc.protocols import com.connectrpc.Code import com.connectrpc.Codec -import com.connectrpc.ConnectError import com.connectrpc.ConnectErrorDetail +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.Idempotency import com.connectrpc.Interceptor @@ -95,8 +95,8 @@ internal class ConnectInterceptor( val responseHeaders = response.headers.filter { entry -> !entry.key.startsWith("trailer-") } val compressionPool = clientConfig.compressionPool(responseHeaders[CONTENT_ENCODING]?.first()) - val (code, connectError) = if (response.code != Code.OK) { - val error = parseConnectUnaryError(code = response.code, response.headers, response.message.buffer) + val (code, exception) = if (response.code != Code.OK) { + val error = parseConnectUnaryException(code = response.code, response.headers, response.message.buffer) error.code to error } else { response.code to null @@ -107,7 +107,7 @@ internal class ConnectInterceptor( message = message, headers = responseHeaders, trailers = trailers, - error = response.error ?: connectError, + cause = response.cause ?: exception, tracingInfo = response.tracingInfo, ) }, @@ -169,8 +169,8 @@ internal class ConnectInterceptor( }, onCompletion = { result -> val streamTrailers = result.trailers - val error = result.connectError() - StreamResult.Complete(error?.code ?: Code.OK, error = error, streamTrailers) + val error = result.connectException() + StreamResult.Complete(error?.code ?: Code.OK, cause = error, streamTrailers) }, ) streamResult @@ -226,7 +226,7 @@ internal class ConnectInterceptor( val code = Code.fromName(endStreamResponseJSON.error.code) StreamResult.Complete( code = code, - error = ConnectError( + cause = ConnectException( code = code, errorDetailParser = serializationStrategy.errorDetailParser(), message = endStreamResponseJSON.error.message, @@ -237,24 +237,24 @@ internal class ConnectInterceptor( } } - private fun parseConnectUnaryError(code: Code, headers: Headers, source: Buffer?): ConnectError { + private fun parseConnectUnaryException(code: Code, headers: Headers, source: Buffer?): ConnectException { if (source == null) { - return ConnectError(code, serializationStrategy.errorDetailParser(), "empty error message from source") + return ConnectException(code, serializationStrategy.errorDetailParser(), "empty error message from source") } return source.use { bufferedSource -> val adapter = moshi.adapter(ErrorPayloadJSON::class.java) val errorJSON = bufferedSource.readUtf8() val errorPayloadJSON = try { - adapter.fromJson(errorJSON) ?: return ConnectError( + adapter.fromJson(errorJSON) ?: return ConnectException( code, serializationStrategy.errorDetailParser(), errorJSON, ) } catch (e: Throwable) { - return ConnectError(code, serializationStrategy.errorDetailParser(), errorJSON) + return ConnectException(code, serializationStrategy.errorDetailParser(), errorJSON) } val errorDetails = parseErrorDetails(errorPayloadJSON) - ConnectError( + ConnectException( code = Code.fromName(errorPayloadJSON.code), errorDetailParser = serializationStrategy.errorDetailParser(), message = errorPayloadJSON.message, diff --git a/library/src/main/kotlin/com/connectrpc/protocols/GRPCCompletion.kt b/library/src/main/kotlin/com/connectrpc/protocols/GRPCCompletion.kt index 6bac2538..e82cf82c 100644 --- a/library/src/main/kotlin/com/connectrpc/protocols/GRPCCompletion.kt +++ b/library/src/main/kotlin/com/connectrpc/protocols/GRPCCompletion.kt @@ -15,8 +15,8 @@ package com.connectrpc.protocols import com.connectrpc.Code -import com.connectrpc.ConnectError import com.connectrpc.ConnectErrorDetail +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.SerializationStrategy import okio.ByteString @@ -35,23 +35,27 @@ internal data class GRPCCompletion( val errorDetails: List, // Set to either message headers (or trailers) where the gRPC status was found. val metadata: Headers, -) +) { + /** + * Converts a completion into a [ConnectException] if the completion failed or if a throwable is passed + * @return a ConnectException on failure, null otherwise + */ + fun toConnectExceptionOrNull(serializationStrategy: SerializationStrategy, cause: Throwable? = null): ConnectException? { + if (cause is ConnectException) { + return cause + } -internal fun grpcCompletionToConnectError(completion: GRPCCompletion?, serializationStrategy: SerializationStrategy, error: Throwable?): ConnectError? { - if (error is ConnectError) { - return error + if (cause != null || code != Code.OK) { + return ConnectException( + code = code, + errorDetailParser = serializationStrategy.errorDetailParser(), + message = message.utf8(), + exception = cause, + details = errorDetails, + metadata = metadata, + ) + } + // Successful call. + return null } - val code = completion?.code ?: Code.UNKNOWN - if (error != null || code != Code.OK) { - return ConnectError( - code = code, - errorDetailParser = serializationStrategy.errorDetailParser(), - message = completion?.message?.utf8(), - exception = error, - details = completion?.errorDetails ?: emptyList(), - metadata = completion?.metadata ?: emptyMap(), - ) - } - // Successful call. - return null } diff --git a/library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt b/library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt index 9265203d..5ac12fb5 100644 --- a/library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt +++ b/library/src/main/kotlin/com/connectrpc/protocols/GRPCInterceptor.kt @@ -15,7 +15,7 @@ package com.connectrpc.protocols import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.Interceptor import com.connectrpc.ProtocolClientConfig @@ -78,7 +78,7 @@ internal class GRPCInterceptor( headers = headers, message = Buffer(), trailers = trailers, - error = response.error, + cause = response.cause, tracingInfo = response.tracingInfo, ) } @@ -94,7 +94,7 @@ internal class GRPCInterceptor( headers = headers, message = message, trailers = trailers, - error = response.error, + cause = response.cause, tracingInfo = response.tracingInfo, ) } else { @@ -108,7 +108,7 @@ internal class GRPCInterceptor( headers = headers, message = result, trailers = trailers, - error = ConnectError( + cause = ConnectException( code = code, errorDetailParser = serializationStrategy.errorDetailParser(), message = completion?.message?.utf8(), @@ -136,20 +136,22 @@ internal class GRPCInterceptor( Envelope.pack(buffer, requestCompression?.compressionPool, requestCompression?.minBytes) }, streamResultFunction = { res -> - val streamResult = res.fold( + res.fold( onHeaders = { result -> val headers = result.headers val completion = completionParser.parse(headers, emptyMap()) if (completion != null) { - val connectError = grpcCompletionToConnectError(completion, serializationStrategy, null) - return@fold StreamResult.Complete( - code = connectError?.code ?: Code.OK, - error = connectError, + val exception = completion.toConnectExceptionOrNull(serializationStrategy) + StreamResult.Complete( + code = exception?.code ?: Code.OK, + cause = exception, trailers = headers, ) + } else { + responseCompressionPool = clientConfig + .compressionPool(headers[GRPC_ENCODING]?.first()) + StreamResult.Headers(headers) } - responseCompressionPool = clientConfig.compressionPool(headers[GRPC_ENCODING]?.first()) - StreamResult.Headers(headers) }, onMessage = { result -> val (_, unpackedMessage) = Envelope.unpackWithHeaderByte( @@ -161,15 +163,21 @@ internal class GRPCInterceptor( onCompletion = { result -> val trailers = result.trailers val completion = completionParser.parse(emptyMap(), trailers) - val connectError = grpcCompletionToConnectError(completion, serializationStrategy, result.error) - StreamResult.Complete( - code = connectError?.code ?: Code.OK, - error = connectError, - trailers = trailers, - ) + if (completion != null) { + val exception = completion.toConnectExceptionOrNull( + serializationStrategy, + result.cause, + ) + StreamResult.Complete( + code = exception?.code ?: Code.OK, + cause = exception, + trailers = trailers, + ) + } else { + result + } }, ) - streamResult }, ) } diff --git a/library/src/main/kotlin/com/connectrpc/protocols/GRPCWebInterceptor.kt b/library/src/main/kotlin/com/connectrpc/protocols/GRPCWebInterceptor.kt index 816260fc..119cade7 100644 --- a/library/src/main/kotlin/com/connectrpc/protocols/GRPCWebInterceptor.kt +++ b/library/src/main/kotlin/com/connectrpc/protocols/GRPCWebInterceptor.kt @@ -15,7 +15,7 @@ package com.connectrpc.protocols import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.Headers import com.connectrpc.Interceptor import com.connectrpc.ProtocolClientConfig @@ -79,7 +79,7 @@ internal class GRPCWebInterceptor( headers = headers, message = Buffer(), trailers = emptyMap(), - error = response.error, + cause = response.cause, tracingInfo = response.tracingInfo, ) } @@ -106,7 +106,7 @@ internal class GRPCWebInterceptor( headers = headers, message = result, trailers = trailers, - error = ConnectError( + cause = ConnectException( code = code, errorDetailParser = serializationStrategy.errorDetailParser(), message = completion?.message?.utf8(), @@ -146,7 +146,7 @@ internal class GRPCWebInterceptor( val result = Buffer() val errorMessage = completionWithMessage.message result.write(errorMessage) - ConnectError( + ConnectException( code = finalCode, errorDetailParser = serializationStrategy.errorDetailParser(), message = errorMessage.utf8(), @@ -160,7 +160,7 @@ internal class GRPCWebInterceptor( headers = headers, message = unpacked, trailers = finalTrailers, - error = error, + cause = error, tracingInfo = response.tracingInfo, ) } @@ -189,10 +189,9 @@ internal class GRPCWebInterceptor( responseCompressionPool = clientConfig.compressionPool(responseHeaders[GRPC_ENCODING]?.first()) val completion = completionParser.parse(responseHeaders, emptyMap()) if (completion != null) { - val connectError = grpcCompletionToConnectError(completion, serializationStrategy, null) return@fold StreamResult.Complete( code = completion.code, - error = connectError, + cause = completion.toConnectExceptionOrNull(serializationStrategy), trailers = responseHeaders, ) } @@ -207,10 +206,9 @@ internal class GRPCWebInterceptor( val streamTrailers = parseGrpcWebTrailer(unpackedMessage) val completion = completionParser.parse(emptyMap(), streamTrailers) val code = completion!!.code - val connectError = grpcCompletionToConnectError(completion, serializationStrategy, null) return@fold StreamResult.Complete( code = code, - error = connectError, + cause = completion.toConnectExceptionOrNull(serializationStrategy), trailers = streamTrailers, ) } diff --git a/library/src/test/kotlin/com/connectrpc/ConnectErrorTest.kt b/library/src/test/kotlin/com/connectrpc/ConnectExceptionTest.kt similarity index 86% rename from library/src/test/kotlin/com/connectrpc/ConnectErrorTest.kt rename to library/src/test/kotlin/com/connectrpc/ConnectExceptionTest.kt index 68a50da8..bbdb9cb5 100644 --- a/library/src/test/kotlin/com/connectrpc/ConnectErrorTest.kt +++ b/library/src/test/kotlin/com/connectrpc/ConnectExceptionTest.kt @@ -20,7 +20,7 @@ import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -class ConnectErrorTest { +class ConnectExceptionTest { private val errorDetailParser: ErrorDetailParser = mock { } @Test @@ -30,7 +30,7 @@ class ConnectErrorTest { "value".encodeUtf8(), ) whenever(errorDetailParser.unpack(errorDetail.pb, String::class)).thenReturn("unpacked_value") - val connectError = ConnectError( + val connectException = ConnectException( code = Code.UNKNOWN, details = listOf( errorDetail, @@ -38,7 +38,7 @@ class ConnectErrorTest { ), errorDetailParser = errorDetailParser, ) - val parsedResult = connectError.unpackedDetails(String::class) + val parsedResult = connectException.unpackedDetails(String::class) assertThat(parsedResult).contains("unpacked_value", "unpacked_value") } @@ -49,13 +49,13 @@ class ConnectErrorTest { "value".encodeUtf8(), ) whenever(errorDetailParser.unpack(errorDetail.pb, String::class)).thenReturn("unpacked_value") - val connectError = ConnectError( + val connectException = ConnectException( code = Code.UNKNOWN, details = listOf( errorDetail, ), ) - val parsedResult = connectError.unpackedDetails(String::class) + val parsedResult = connectException.unpackedDetails(String::class) assertThat(parsedResult).isEmpty() } } diff --git a/library/src/test/kotlin/com/connectrpc/InterceptorChainTest.kt b/library/src/test/kotlin/com/connectrpc/InterceptorChainTest.kt index 213fe198..23f47a7f 100644 --- a/library/src/test/kotlin/com/connectrpc/InterceptorChainTest.kt +++ b/library/src/test/kotlin/com/connectrpc/InterceptorChainTest.kt @@ -126,7 +126,7 @@ class InterceptorChainTest { it.message, it.trailers, it.tracingInfo, - it.error, + it.cause, ) }, ) diff --git a/library/src/test/kotlin/com/connectrpc/protocols/ConnectInterceptorTest.kt b/library/src/test/kotlin/com/connectrpc/protocols/ConnectInterceptorTest.kt index 6d8c3b5f..6f1982fa 100644 --- a/library/src/test/kotlin/com/connectrpc/protocols/ConnectInterceptorTest.kt +++ b/library/src/test/kotlin/com/connectrpc/protocols/ConnectInterceptorTest.kt @@ -16,7 +16,7 @@ package com.connectrpc.protocols import com.connectrpc.Code import com.connectrpc.Codec -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.ErrorDetailParser import com.connectrpc.Idempotency import com.connectrpc.Method.GET_METHOD @@ -245,9 +245,9 @@ class ConnectInterceptorTest { tracingInfo = null, ), ) - assertThat(response.error!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) - assertThat(response.error!!.message).isEqualTo("no more resources!") - val connectErrorDetail = response.error!!.details.singleOrNull()!! + assertThat(response.cause!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(response.cause!!.message).isEqualTo("no more resources!") + val connectErrorDetail = response.cause!!.details.singleOrNull()!! assertThat(connectErrorDetail.type).isEqualTo("type") assertThat(connectErrorDetail.payload).isEqualTo("value".encodeUtf8()) } @@ -271,7 +271,7 @@ class ConnectInterceptorTest { tracingInfo = null, ), ) - assertThat(response.error!!.code).isEqualTo(Code.UNAVAILABLE) + assertThat(response.cause!!.code).isEqualTo(Code.UNAVAILABLE) } @Test @@ -533,9 +533,9 @@ class ConnectInterceptorTest { assertThat(result).isOfAnyClassIn(StreamResult.Complete::class.java) val completion = result as StreamResult.Complete assertThat(completion.code).isEqualTo(Code.RESOURCE_EXHAUSTED) - val connectError = completion.connectError()!! - assertThat(connectError.message).isEqualTo("no more resources!") - val errorDetail = connectError.details.single() + val exception = completion.connectException()!! + assertThat(exception.message).isEqualTo("no more resources!") + val errorDetail = exception.details.single() assertThat(errorDetail.type).isEqualTo("type") assertThat(errorDetail.payload).isEqualTo("value".encodeUtf8()) } @@ -595,7 +595,7 @@ class ConnectInterceptorTest { val result = streamFunction.streamResultFunction( StreamResult.Complete( code = Code.UNKNOWN, - error = ConnectError( + cause = ConnectException( Code.UNKNOWN, message = "error_message", ), diff --git a/library/src/test/kotlin/com/connectrpc/protocols/GRPCInterceptorTest.kt b/library/src/test/kotlin/com/connectrpc/protocols/GRPCInterceptorTest.kt index 929ab80f..83375274 100644 --- a/library/src/test/kotlin/com/connectrpc/protocols/GRPCInterceptorTest.kt +++ b/library/src/test/kotlin/com/connectrpc/protocols/GRPCInterceptorTest.kt @@ -15,8 +15,8 @@ package com.connectrpc.protocols import com.connectrpc.Code -import com.connectrpc.ConnectError import com.connectrpc.ConnectErrorDetail +import com.connectrpc.ConnectException import com.connectrpc.ErrorDetailParser import com.connectrpc.MethodSpec import com.connectrpc.ProtocolClientConfig @@ -259,9 +259,9 @@ class GRPCInterceptorTest { tracingInfo = null, ), ) - assertThat(response.error!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) - assertThat(response.error!!.message).isEqualTo("no more resources!") - val connectErrorDetail = response.error!!.details.singleOrNull()!! + assertThat(response.cause!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(response.cause!!.message).isEqualTo("no more resources!") + val connectErrorDetail = response.cause!!.details.singleOrNull()!! assertThat(connectErrorDetail.type).isEqualTo("type") assertThat(connectErrorDetail.payload).isEqualTo("value".encodeUtf8()) } @@ -519,7 +519,7 @@ class GRPCInterceptorTest { val result = streamFunction.streamResultFunction( StreamResult.Complete( code = Code.UNKNOWN, - error = ConnectError( + cause = ConnectException( Code.UNKNOWN, message = "error_message", ), diff --git a/library/src/test/kotlin/com/connectrpc/protocols/GRPCWebInterceptorTest.kt b/library/src/test/kotlin/com/connectrpc/protocols/GRPCWebInterceptorTest.kt index 6ed54088..b165991e 100644 --- a/library/src/test/kotlin/com/connectrpc/protocols/GRPCWebInterceptorTest.kt +++ b/library/src/test/kotlin/com/connectrpc/protocols/GRPCWebInterceptorTest.kt @@ -15,7 +15,7 @@ package com.connectrpc.protocols import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.ErrorDetailParser import com.connectrpc.MethodSpec import com.connectrpc.ProtocolClientConfig @@ -225,7 +225,7 @@ class GRPCWebInterceptorTest { tracingInfo = null, ), ) - assertThat(response.error!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(response.cause!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) } @Test @@ -284,7 +284,7 @@ class GRPCWebInterceptorTest { tracingInfo = null, ), ) - assertThat(response.error!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) + assertThat(response.cause!!.code).isEqualTo(Code.RESOURCE_EXHAUSTED) } @Test @@ -520,8 +520,8 @@ class GRPCWebInterceptorTest { assertThat(result).isOfAnyClassIn(StreamResult.Complete::class.java) val completion = result as StreamResult.Complete assertThat(completion.code).isEqualTo(Code.RESOURCE_EXHAUSTED) - val connectError = completion.connectError()!! - assertThat(connectError.message).isEqualTo("no more resources!") + val exception = completion.connectException()!! + assertThat(exception.message).isEqualTo("no more resources!") } @Test @@ -560,7 +560,7 @@ class GRPCWebInterceptorTest { val result = streamFunction.streamResultFunction( StreamResult.Complete( code = Code.UNKNOWN, - error = ConnectError( + cause = ConnectException( Code.UNKNOWN, message = "error_message", ), diff --git a/okhttp/src/main/kotlin/com/connectrpc/okhttp/ConnectOkHttpClient.kt b/okhttp/src/main/kotlin/com/connectrpc/okhttp/ConnectOkHttpClient.kt index 8edbe292..0af47ece 100644 --- a/okhttp/src/main/kotlin/com/connectrpc/okhttp/ConnectOkHttpClient.kt +++ b/okhttp/src/main/kotlin/com/connectrpc/okhttp/ConnectOkHttpClient.kt @@ -15,7 +15,7 @@ package com.connectrpc.okhttp import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.StreamResult import com.connectrpc.http.Cancelable import com.connectrpc.http.HTTPClientInterface @@ -74,7 +74,7 @@ class ConnectOkHttpClient @JvmOverloads constructor( headers = emptyMap(), message = Buffer(), trailers = emptyMap(), - error = ConnectError( + cause = ConnectException( code, message = e.message, exception = e, @@ -110,7 +110,7 @@ class ConnectOkHttpClient @JvmOverloads constructor( headers = emptyMap(), message = Buffer(), trailers = emptyMap(), - error = ConnectError( + cause = ConnectException( Code.UNKNOWN, message = e.message, exception = e, diff --git a/okhttp/src/main/kotlin/com/connectrpc/okhttp/OkHttpStream.kt b/okhttp/src/main/kotlin/com/connectrpc/okhttp/OkHttpStream.kt index 971d10f7..1a06abe1 100644 --- a/okhttp/src/main/kotlin/com/connectrpc/okhttp/OkHttpStream.kt +++ b/okhttp/src/main/kotlin/com/connectrpc/okhttp/OkHttpStream.kt @@ -15,7 +15,7 @@ package com.connectrpc.okhttp import com.connectrpc.Code -import com.connectrpc.ConnectError +import com.connectrpc.ConnectException import com.connectrpc.StreamResult import com.connectrpc.http.HTTPRequest import com.connectrpc.http.Stream @@ -87,12 +87,12 @@ private class ResponseCallback( runBlocking { if (e is InterruptedIOException) { if (e.message == "timeout") { - val error = ConnectError(code = Code.DEADLINE_EXCEEDED) - onResult(StreamResult.Complete(Code.DEADLINE_EXCEEDED, error = error)) + val error = ConnectException(code = Code.DEADLINE_EXCEEDED) + onResult(StreamResult.Complete(Code.DEADLINE_EXCEEDED, cause = error)) return@runBlocking } } - onResult(StreamResult.Complete(Code.UNKNOWN, error = e)) + onResult(StreamResult.Complete(Code.UNKNOWN, cause = e)) } } @@ -106,7 +106,7 @@ private class ResponseCallback( val finalResult = StreamResult.Complete( code = code, trailers = response.safeTrailers() ?: emptyMap(), - error = ConnectError(code = code), + cause = ConnectException(code = code), ) onResult(finalResult) return@runBlocking @@ -130,7 +130,7 @@ private class ResponseCallback( val finalResult = StreamResult.Complete( code = code, trailers = response.safeTrailers() ?: emptyMap(), - error = exception, + cause = exception, ) onResult(finalResult) }