Skip to content

Commit 07d7e88

Browse files
authored
Exception Deserialization (#170)
1 parent a499a32 commit 07d7e88

File tree

11 files changed

+80
-22
lines changed

11 files changed

+80
-22
lines changed

core/src/commonMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public fun serializeException(cause: Throwable): SerializedException {
1212
val message = cause.message ?: "Unknown exception"
1313
val stacktrace = cause.stackElements()
1414
val serializedCause = cause.cause?.let { serializeException(it) }
15-
val className = cause::class.qualifiedClassNameOrNull ?: ""
15+
val className = cause::class.typeName ?: ""
1616

1717
return SerializedException(cause.toString(), message, stacktrace, serializedCause, className)
1818
}

core/src/commonMain/kotlin/kotlinx/rpc/internal/ReflectionUtils.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public fun internalError(message: String): Nothing {
2121
error("Internal kotlinx.rpc error: $message")
2222
}
2323

24+
@InternalRPCApi
25+
public expect val KClass<*>.typeName: String?
26+
2427
@InternalRPCApi
2528
public expect val KClass<*>.qualifiedClassNameOrNull: String?
2629

core/src/jsMain/kotlin/kotlinx/rpc/internal/ReflectionUtils.js.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ import kotlin.reflect.KClass
1111
@InternalRPCApi
1212
public actual val KClass<*>.qualifiedClassNameOrNull: String?
1313
get() = toString()
14+
15+
@InternalRPCApi
16+
public actual val KClass<*>.typeName: String?
17+
get() = qualifiedClassNameOrNull

core/src/jvmMain/kotlin/kotlinx/rpc/internal/ExceptionUtils.jvm.kt

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,44 @@ public actual fun SerializedException.deserialize(): Throwable {
6262
private fun Class<*>.fieldsCountOrDefault(defaultValue: Int) =
6363
kotlin.runCatching { fieldsCount() }.getOrDefault(defaultValue)
6464

65-
private tailrec fun Class<*>.fieldsCount(accumulator: Int = 0): Int {
66-
val fieldsCount = declaredFields.count { !Modifier.isStatic(it.modifiers) }
67-
val totalFields = accumulator + fieldsCount
68-
val superClass = superclass ?: return totalFields
65+
private tailrec fun Class<*>.fieldsCount(accumulator: Set<String> = emptySet()): Int {
66+
val fields = declaredFields
67+
.filter { !Modifier.isStatic(it.modifiers) }
68+
.map { it.name }
69+
val totalFields = (accumulator + fields)
70+
val superClass = superclass ?: run {
71+
var messageField = false
72+
return totalFields.count {
73+
// Throwable has a private field 'detailMessage', but a public open `getMessage`,
74+
// which are the same in custom Kotlin exceptions
75+
if (it == "message" || it == "detailMessage") {
76+
if (messageField) {
77+
return@count false
78+
}
79+
messageField = true
80+
}
81+
true
82+
}
83+
}
6984
return superClass.fieldsCount(totalFields)
7085
}
7186

87+
private fun Class<*>.isExceptionClass(): Boolean = Throwable::class.java.isAssignableFrom(this)
88+
7289
private fun tryCreateException(constructor: Constructor<*>, serialized: SerializedException): Throwable? {
7390
val parameters = constructor.parameterTypes
7491

7592
val result = when (parameters.size) {
76-
2 -> constructor.newInstance(serialized.message, serialized.cause?.deserialize())
77-
1 -> when (parameters[0]) {
78-
Throwable::class.java -> constructor.newInstance(serialized.cause?.deserialize())
79-
String::class.java -> constructor.newInstance(serialized.message)
93+
2 -> when {
94+
parameters[0] == String::class.java && parameters[1].isExceptionClass() ->
95+
constructor.newInstance(serialized.message, serialized.cause?.deserialize())
96+
97+
else -> null
98+
}
99+
100+
1 -> when {
101+
parameters[0].isExceptionClass() -> constructor.newInstance(serialized.cause?.deserialize())
102+
parameters[0] == String::class.java -> constructor.newInstance(serialized.message)
80103
else -> null
81104
}
82105

core/src/jvmMain/kotlin/kotlinx/rpc/internal/ReflectionUtils.jvm.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ import kotlin.reflect.KClass
1111
@InternalRPCApi
1212
public actual val KClass<*>.qualifiedClassNameOrNull: String?
1313
get() = qualifiedName
14+
15+
@InternalRPCApi
16+
public actual val KClass<*>.typeName: String?
17+
get() = java.typeName

core/src/nativeMain/kotlin/kotlinx/rpc/internal/ReflectionUtils.native.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ import kotlin.reflect.KClass
1111
@InternalRPCApi
1212
public actual val KClass<*>.qualifiedClassNameOrNull: String?
1313
get() = qualifiedName
14+
15+
@InternalRPCApi
16+
public actual val KClass<*>.typeName: String?
17+
get() = qualifiedClassNameOrNull

gradle-conventions-settings/src/main/kotlin/util/PublicationUtils.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,19 @@ fun MavenPublication.fixModuleMetadata(project: Project) {
3434
val upperCaseName = Character.toUpperCase(publicationName.first()) + publicationName.drop(1)
3535
val patchTaskName = "patchModuleJsonFor$upperCaseName"
3636
val generateMetadataTaskName = "generateMetadataFileFor${upperCaseName}Publication"
37+
val projectName = project.name
38+
val projectVersion = project.version.toString()
39+
val moduleJsonFile = project.layout.buildDirectory.file("publications/$publicationName/module.json").get().asFile
3740

3841
val patch = project.tasks.register(patchTaskName) {
3942
group = "patch"
4043

4144
doLast {
42-
val file = project.layout.buildDirectory.file("publications/$publicationName/module.json").get().asFile
43-
if (!file.exists()) {
45+
if (!moduleJsonFile.exists()) {
4446
return@doLast
4547
}
4648

47-
file.fixMetadata(project)
49+
moduleJsonFile.fixMetadata(projectName, projectVersion)
4850

4951
logger.info("Updated metadata for $publicationName publication")
5052
}
@@ -55,13 +57,13 @@ fun MavenPublication.fixModuleMetadata(project: Project) {
5557
}
5658
}
5759

58-
private fun File.fixMetadata(project: Project) {
60+
private fun File.fixMetadata(projectName: String, projectVersion: String) {
5961
val text = readText()
6062

6163
val newText = text
62-
.updateFilesNameField(project.name)
63-
.updateAvailableAtModuleField(project.name)
64-
.updateAvailableAtUrlField(project.name, project.version.toString())
64+
.updateFilesNameField(projectName)
65+
.updateAvailableAtModuleField(projectName)
66+
.updateAvailableAtUrlField(projectName, projectVersion)
6567

6668
writeText(newText)
6769
}

krpc/krpc-test/src/jvmMain/kotlin/kotlinx/rpc/test/KRPCTestService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface KRPCTestService : RPC {
5555
suspend fun nullableBytes(byteArray: ByteArray?)
5656

5757
suspend fun throwsIllegalArgument(message: String)
58+
suspend fun throwsSerializableWithMessageAndCause(message: String)
5859
suspend fun throwsThrowable(message: String)
5960
suspend fun throwsUNSTOPPABLEThrowable(message: String)
6061

krpc/krpc-test/src/jvmMain/kotlin/kotlinx/rpc/test/KRPCTestServiceBackend.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.rpc.test
66

77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.flow.*
9+
import kotlinx.serialization.Serializable
910
import kotlin.coroutines.CoroutineContext
1011
import kotlin.coroutines.resumeWithException
1112
import kotlin.test.assertEquals
@@ -184,6 +185,16 @@ class KRPCTestServiceBackend(override val coroutineContext: CoroutineContext) :
184185
throw IllegalArgumentException(message)
185186
}
186187

188+
@Serializable
189+
class SerializableTestException(
190+
override val message: String?,
191+
override val cause: SerializableTestException? = null,
192+
) : Exception()
193+
194+
override suspend fun throwsSerializableWithMessageAndCause(message: String) {
195+
throw SerializableTestException(message, SerializableTestException("cause: $message"))
196+
}
197+
187198
@Suppress("detekt.TooGenericExceptionThrown")
188199
override suspend fun throwsThrowable(message: String) {
189200
throw Throwable(message)

krpc/krpc-test/src/jvmMain/kotlin/kotlinx/rpc/test/KRPCTransportTestBase.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.*
1212
import kotlinx.rpc.*
1313
import kotlinx.rpc.serialization.RPCSerialFormatConfiguration
1414
import kotlinx.rpc.server.KRPCServer
15+
import kotlinx.rpc.test.KRPCTestServiceBackend.SerializableTestException
1516
import org.junit.Assert.assertEquals
1617
import org.junit.Rule
1718
import org.junit.rules.Timeout
@@ -370,16 +371,21 @@ abstract class KRPCTransportTestBase {
370371
}
371372
}
372373

373-
@OptIn(ExperimentalCoroutinesApi::class)
374374
@Test
375375
@Suppress("detekt.TooGenericExceptionCaught")
376-
fun testException() {
376+
fun testExceptionSerializationAndPropagating() {
377377
runBlocking {
378378
try {
379379
client.throwsIllegalArgument("me")
380380
} catch (e: IllegalArgumentException) {
381381
assertEquals("me", e.message)
382382
}
383+
try {
384+
client.throwsSerializableWithMessageAndCause("me")
385+
} catch (e: SerializableTestException) {
386+
assertEquals("me", e.message)
387+
assertEquals("cause: me", e.cause?.message)
388+
}
383389
try {
384390
client.throwsThrowable("me")
385391
} catch (e: Throwable) {

0 commit comments

Comments
 (0)