diff --git a/grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/internal/suspendClientCalls.kt b/grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/internal/suspendClientCalls.kt index 2fef24f64..6859da34c 100644 --- a/grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/internal/suspendClientCalls.kt +++ b/grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/internal/suspendClientCalls.kt @@ -22,6 +22,7 @@ import kotlinx.rpc.grpc.GrpcMetadata import kotlinx.rpc.grpc.Status import kotlinx.rpc.grpc.StatusCode import kotlinx.rpc.grpc.StatusException +import kotlinx.rpc.grpc.cause import kotlinx.rpc.grpc.client.ClientCallScope import kotlinx.rpc.grpc.client.GrpcCallOptions import kotlinx.rpc.grpc.client.GrpcClient @@ -284,7 +285,7 @@ private class ClientCallScopeImpl( onClose = { status: Status, trailers: GrpcMetadata -> var cause = when { status.statusCode == StatusCode.OK -> null - status.getCause() is CancellationException -> status.getCause() + status.cause is CancellationException -> status.cause else -> StatusException(status, trailers) } diff --git a/grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.native.kt b/grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.native.kt index 56ad87de7..6a20bbfba 100644 --- a/grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.native.kt +++ b/grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.native.kt @@ -17,6 +17,7 @@ import kotlinx.rpc.grpc.GrpcMetadata import kotlinx.rpc.grpc.StatusException import kotlinx.rpc.grpc.internal.destroyEntries import kotlinx.rpc.grpc.internal.toRaw +import kotlinx.rpc.grpc.status import kotlinx.rpc.grpc.statusCode import libkgrpc.* import platform.posix.size_tVar @@ -87,7 +88,7 @@ private fun getMetadataCallback( } notifyResult(metadata, grpc_status_code.GRPC_STATUS_OK, null) } catch (e: StatusException) { - notifyResult(metadata, e.getStatus().statusCode.toRaw(), e.message) + notifyResult(metadata, e.status.statusCode.toRaw(), e.message) } catch (e: CancellationException) { notifyResult(metadata, grpc_status_code.GRPC_STATUS_CANCELLED, e.message) throw e diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt index 66e452cb7..66fa2902a 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt @@ -6,6 +6,8 @@ package kotlinx.rpc.grpc +import kotlinx.rpc.internal.utils.InternalRpcApi + /** * Defines the status of an operation by providing a standard [StatusCode] in conjunction with an * optional descriptive message. @@ -26,14 +28,39 @@ package kotlinx.rpc.grpc * [doc/statuscodes.md](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md) */ public expect class Status { - public fun getDescription(): String? - public fun getCause(): Throwable? + internal fun getDescription(): String? + internal fun getCause(): Throwable? } +/** + * Creates a [Status] with the specified [code], optional [description], and [cause]. + */ public expect fun Status(code: StatusCode, description: String? = null, cause: Throwable? = null): Status +/** + * The status code of this status. + */ public expect val Status.statusCode: StatusCode +/** + * The description of this status, or null if not present. + */ +public val Status.description: String? get() = getDescription() + +// this is currently @InternalRpcApi as it's behavior would be inconsistent between JVM and Native. +@InternalRpcApi +public val Status.cause: Throwable? get() = getCause() + +/** + * Converts this status to a [StatusException] with optional [trailers]. + */ +public fun Status.asException(trailers: GrpcMetadata? = null): StatusException { + return StatusException(this, trailers) +} + +/** + * Standard gRPC status codes. + */ public enum class StatusCode(public val value: Int) { OK(0), CANCELLED(1), @@ -53,5 +80,23 @@ public enum class StatusCode(public val value: Int) { DATA_LOSS(15), UNAUTHENTICATED(16); + /** + * The ASCII-encoded byte representation of the status code value. + */ public val valueAscii: ByteArray = value.toString().encodeToByteArray() + + /** + * Converts this status code to a [Status] with an optional [description]. + */ + public fun asStatus(description: String? = null): Status { + return Status(this, description) + } + + /** + * Converts this status code to a [StatusException] with optional [description] and [trailers]. + */ + public fun asException(description: String? = null, trailers: GrpcMetadata? = null): StatusException { + return StatusException(Status(this, description), trailers) + } } + diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusException.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusException.kt index eae3df45e..2de9f612b 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusException.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusException.kt @@ -4,21 +4,56 @@ package kotlinx.rpc.grpc +import kotlinx.rpc.internal.utils.InternalRpcApi + /** - * [Status] in Exception form, for propagating Status information via exceptions. + * Exception used for propagating gRPC status information in non-OK results. + * + * This is the primary mechanism for reporting and handling errors in gRPC calls. + * When a server encounters an error, it typically throws a [StatusException] with an appropriate + * [StatusCode] to signal the failure to the client. Clients receive this exception when + * remote calls fail with a non-OK status. + * + * The easiest way to construct a [StatusException] is to use the [StatusCode.asException] extension function: + * ``` + * throw StatusCode.UNAUTHORIZED.asException("Authentication failed") + * ``` + * + * @see Status + * @see StatusCode */ public expect class StatusException : Exception { public constructor(status: Status) public constructor(status: Status, trailers: GrpcMetadata?) - public fun getStatus(): Status - public fun getTrailers(): GrpcMetadata? + internal fun getStatus(): Status + internal fun getTrailers(): GrpcMetadata? } +/** + * The status associated with this exception. + */ +public val StatusException.status: Status get() = getStatus() + +/** + * The trailing metadata associated with this exception, or null if not present. + */ +public val StatusException.trailers: GrpcMetadata? get() = getTrailers() + +@InternalRpcApi public expect class StatusRuntimeException : RuntimeException { - public constructor(status: Status) - public constructor(status: Status, trailers: GrpcMetadata?) + internal constructor(status: Status, trailers: GrpcMetadata?) - public fun getStatus(): Status - public fun getTrailers(): GrpcMetadata? + internal fun getStatus(): Status + internal fun getTrailers(): GrpcMetadata? } + +@InternalRpcApi +public fun StatusRuntimeException(code: StatusCode, description: String? = null, trailers: GrpcMetadata? = null): StatusRuntimeException { + return StatusRuntimeException(Status(code, description), trailers) +} + +@InternalRpcApi +public val StatusRuntimeException.status: Status get() = getStatus() +@InternalRpcApi +public val StatusRuntimeException.trailers: GrpcMetadata? get() = getTrailers() \ No newline at end of file diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/suspendUtils.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/suspendUtils.kt index b8d0be409..e3450125f 100644 --- a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/suspendUtils.kt +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/suspendUtils.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.single import kotlinx.rpc.grpc.Status import kotlinx.rpc.grpc.StatusCode import kotlinx.rpc.grpc.StatusException +import kotlinx.rpc.grpc.asException import kotlinx.rpc.internal.utils.InternalRpcApi @InternalRpcApi @@ -25,16 +26,12 @@ public fun Flow.singleOrStatusFlow( found = true emit(it) } else { - throw StatusException( - Status(StatusCode.INTERNAL, "Expected one $expected for $descriptor but received two") - ) + throw StatusCode.INTERNAL.asException("Expected one $expected for $descriptor but received two") } } if (!found) { - throw StatusException( - Status(StatusCode.INTERNAL, "Expected one $expected for $descriptor but received none") - ) + throw StatusCode.INTERNAL.asException("Expected one $expected for $descriptor but received none") } } diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt index af5d32ee7..49b815a5e 100644 --- a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt @@ -7,9 +7,7 @@ package kotlinx.rpc.grpc.test.proto import kotlinx.coroutines.CompletableDeferred import kotlinx.rpc.RpcServer import kotlinx.rpc.grpc.GrpcMetadata -import kotlinx.rpc.grpc.Status import kotlinx.rpc.grpc.StatusCode -import kotlinx.rpc.grpc.StatusException import kotlinx.rpc.grpc.append import kotlinx.rpc.grpc.buildGrpcMetadata import kotlinx.rpc.grpc.client.GrpcCallCredentials @@ -26,6 +24,7 @@ import kotlinx.rpc.grpc.test.SERVER_CERT_PEM import kotlinx.rpc.grpc.test.SERVER_KEY_PEM import kotlinx.rpc.grpc.test.assertGrpcFailure import kotlinx.rpc.grpc.test.invoke +import kotlinx.rpc.grpc.asException import kotlinx.rpc.registerService import kotlinx.rpc.withService import kotlin.coroutines.cancellation.CancellationException @@ -338,7 +337,7 @@ abstract class PlaintextCallCredentials : GrpcCallCredentials { } class ThrowingCallCredentials( - private val exception: Throwable = StatusException(Status(StatusCode.UNIMPLEMENTED, "This is my custom exception")) + private val exception: Throwable = StatusCode.UNIMPLEMENTED.asException("This is my custom exception") ) : PlaintextCallCredentials() { override suspend fun Context.getRequestMetadata(): GrpcMetadata { throw exception diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusException.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusException.native.kt index f5fe62187..2f259d8cd 100644 --- a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusException.native.kt +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusException.native.kt @@ -18,18 +18,16 @@ public actual class StatusException : Exception { this.trailers = trailers } - public actual fun getStatus(): Status = status + internal actual fun getStatus(): Status = status - public actual fun getTrailers(): GrpcMetadata? = trailers + internal actual fun getTrailers(): GrpcMetadata? = trailers } public actual class StatusRuntimeException : RuntimeException { private val status: Status private val trailers: GrpcMetadata? - public actual constructor(status: Status) : this(status, null) - - public actual constructor(status: Status, trailers: GrpcMetadata?) : super( + internal actual constructor(status: Status, trailers: GrpcMetadata?) : super( "${status.statusCode}: ${status.getDescription()}", status.getCause() ) { @@ -37,7 +35,7 @@ public actual class StatusRuntimeException : RuntimeException { this.trailers = trailers } - public actual fun getStatus(): Status = status + internal actual fun getStatus(): Status = status - public actual fun getTrailers(): GrpcMetadata? = trailers + internal actual fun getTrailers(): GrpcMetadata? = trailers } diff --git a/grpc/grpc-server/src/commonMain/kotlin/kotlinx/rpc/grpc/server/internal/suspendServerCalls.kt b/grpc/grpc-server/src/commonMain/kotlin/kotlinx/rpc/grpc/server/internal/suspendServerCalls.kt index 85333614d..640073aeb 100644 --- a/grpc/grpc-server/src/commonMain/kotlin/kotlinx/rpc/grpc/server/internal/suspendServerCalls.kt +++ b/grpc/grpc-server/src/commonMain/kotlin/kotlinx/rpc/grpc/server/internal/suspendServerCalls.kt @@ -30,6 +30,8 @@ import kotlinx.rpc.grpc.internal.singleOrStatusFlow import kotlinx.rpc.grpc.merge import kotlinx.rpc.grpc.server.ServerCallScope import kotlinx.rpc.grpc.server.ServerInterceptor +import kotlinx.rpc.grpc.status +import kotlinx.rpc.grpc.trailers import kotlinx.rpc.internal.utils.InternalRpcApi import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -206,8 +208,8 @@ private fun CoroutineScope.serverCallListenerImpl( val closeStatus = when (failure) { null -> Status(StatusCode.OK) is CancellationException -> Status(StatusCode.CANCELLED, cause = failure) - is StatusException -> failure.getStatus() - is StatusRuntimeException -> failure.getStatus() + is StatusException -> failure.status + is StatusRuntimeException -> failure.status else -> Status(StatusCode.UNKNOWN, cause = failure) } @@ -215,8 +217,8 @@ private fun CoroutineScope.serverCallListenerImpl( // we merge the failure trailers with the user-defined trailers when (failure) { - is StatusException -> failure.getTrailers() - is StatusRuntimeException -> failure.getTrailers() + is StatusException -> failure.trailers + is StatusRuntimeException -> failure.trailers else -> null }?.let { trailers.merge(it) } diff --git a/grpc/grpc-server/src/nativeMain/kotlin/kotlinx/rpc/grpc/server/internal/NativeServerCall.kt b/grpc/grpc-server/src/nativeMain/kotlin/kotlinx/rpc/grpc/server/internal/NativeServerCall.kt index 9fc17e1b9..649f1e4cf 100644 --- a/grpc/grpc-server/src/nativeMain/kotlin/kotlinx/rpc/grpc/server/internal/NativeServerCall.kt +++ b/grpc/grpc-server/src/nativeMain/kotlin/kotlinx/rpc/grpc/server/internal/NativeServerCall.kt @@ -36,6 +36,7 @@ import kotlinx.rpc.grpc.internal.toGrpcByteBuffer import kotlinx.rpc.grpc.internal.toGrpcSlice import kotlinx.rpc.grpc.internal.toKotlin import kotlinx.rpc.grpc.internal.toRaw +import kotlinx.rpc.grpc.status import kotlinx.rpc.grpc.statusCode import kotlinx.rpc.protobuf.input.stream.asInputStream import kotlinx.rpc.protobuf.input.stream.asSource @@ -363,7 +364,7 @@ internal class NativeServerCall( } catch (e: Throwable) { // TODO: Log internal error as warning val status = when (e) { - is StatusException -> e.getStatus() + is StatusException -> e.status else -> Status( StatusCode.INTERNAL, description = "Internal error, so canceling the stream",