Skip to content
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 @@ -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
Expand Down Expand Up @@ -284,7 +285,7 @@ private class ClientCallScopeImpl<Request, Response>(
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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
49 changes: 47 additions & 2 deletions grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the inconsistency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some APIs in Java attach the causing exception. However, on native, we would lose the causing exception information as we can't pass it through the C layer. So I think it might be better to just omit it, so users don't rely on the cause value of the Status exception.

@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),
Expand All @@ -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)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,16 +26,12 @@ public fun <T> Flow<T>.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")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,24 @@ 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()
) {
this.status = status
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -206,17 +208,17 @@ private fun <Request, Response> 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)
}

val trailers = serverCallScope.responseTrailers

// 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) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -363,7 +364,7 @@ internal class NativeServerCall<Request, Response>(
} 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",
Expand Down
Loading