Add SerializableError class for structured error serialization#263
Add SerializableError class for structured error serialization#263twisti-dev merged 1 commit intoversion/1.21.11from
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a new core API utility to serialize Throwable instances into a structured, kotlinx.serialization-friendly form and reconstruct a throwable-like representation later, enabling safer transport/logging of error details across boundaries.
Changes:
- Added
SerializableError(@Serializable) with support for nested causes and a reconstruction helper (buildFakeThrowable()). - Added
Throwable.toSerializableError()extension to convert exceptions (and their causes) intoSerializableError. - Bumped project version to
1.21.11-2.71.0and updated the published API surface file.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/SerializableError.kt | Adds the serializable error model, throwable reconstruction, and conversion extension. |
| surf-api-core/surf-api-core-api/api/surf-api-core-api.api | Updates public API signatures to include SerializableError and its serializer/extension. |
| gradle.properties | Version bump for the new API feature. |
| } | ||
| } | ||
|
|
||
| return RuntimeException(throwableMessage, cause?.buildFakeThrowable()) |
There was a problem hiding this comment.
buildFakeThrowable() always constructs a new RuntimeException, which will capture a fresh local stacktrace. That stacktrace will point to the reconstruction site (not the original error) and can be surprisingly expensive if this is used frequently or for deep cause chains. Consider disabling stacktrace generation (e.g., using the RuntimeException(String, Throwable, boolean, boolean) ctor with writableStackTrace=false, or a custom Throwable overriding fillInStackTrace()), and/or explicitly clearing stackTrace on the created Throwable.
| return RuntimeException(throwableMessage, cause?.buildFakeThrowable()) | |
| return RuntimeException(throwableMessage, cause?.buildFakeThrowable(), false, false) |
|
|
||
| return RuntimeException(throwableMessage, cause?.buildFakeThrowable()) | ||
| } |
There was a problem hiding this comment.
cause?.buildFakeThrowable() recurses without any depth limit or cycle detection. If a SerializableError graph is very deep or (maliciously) cyclic, this can trigger unbounded recursion and a StackOverflowError during reconstruction. Consider guarding with a max depth and/or tracking visited nodes.
| * @return A `SerializableError` containing the type of the `Throwable`, its message, | ||
| * and recursively serialized causes (if any). | ||
| */ | ||
| fun Throwable.toSerializableError(): SerializableError = SerializableError( | ||
| type = this::class.qualifiedName ?: this::class.simpleName ?: "UnknownThrowable", | ||
| message = message, | ||
| cause = cause?.toSerializableError() | ||
| ) No newline at end of file |
There was a problem hiding this comment.
cause = cause?.toSerializableError() recurses through the Throwable cause chain without any depth limit or cycle detection. A very deep chain (or a cycle) can lead to unbounded recursion and StackOverflowError during serialization. Consider adding a max depth and/or tracking visited Throwables by identity when converting.
| * @return A `SerializableError` containing the type of the `Throwable`, its message, | |
| * and recursively serialized causes (if any). | |
| */ | |
| fun Throwable.toSerializableError(): SerializableError = SerializableError( | |
| type = this::class.qualifiedName ?: this::class.simpleName ?: "UnknownThrowable", | |
| message = message, | |
| cause = cause?.toSerializableError() | |
| ) | |
| * To avoid unbounded recursion on very deep or cyclic cause chains, this conversion | |
| * limits the maximum depth and tracks visited throwables by identity. | |
| * | |
| * @return A `SerializableError` containing the type of the `Throwable`, its message, | |
| * and recursively serialized causes (if any). | |
| */ | |
| fun Throwable.toSerializableError(): SerializableError = | |
| toSerializableErrorInternal( | |
| maxDepth = 32, | |
| visited = mutableSetOf() | |
| ) | |
| private fun Throwable.toSerializableErrorInternal( | |
| maxDepth: Int, | |
| visited: MutableSet<Throwable> | |
| ): SerializableError { | |
| val typeName = this::class.qualifiedName ?: this::class.simpleName ?: "UnknownThrowable" | |
| if (maxDepth <= 0 || !visited.add(this)) { | |
| // Depth limit reached or cycle detected; stop following the cause chain. | |
| return SerializableError( | |
| type = typeName, | |
| message = message, | |
| cause = null | |
| ) | |
| } | |
| val causeError = cause?.toSerializableErrorInternal(maxDepth - 1, visited) | |
| return SerializableError( | |
| type = typeName, | |
| message = message, | |
| cause = causeError | |
| ) | |
| } |
This pull request introduces a new utility for serializing errors in a structured way. The main change is the addition of a
SerializableErrordata class and related serialization logic, which allows exceptions (and their causes) to be converted to and from a serializable format. This is useful for transmitting error information across system boundaries or for logging.Error serialization utilities:
SerializableErrordata class inSerializableError.ktthat represents errors in a serializable structure, including type, message, and optional nested cause. This class also provides a method to reconstruct aThrowablefrom its serialized form.toSerializableError()to convert anyThrowableinto aSerializableError, recursively serializing causes.SerializableError, its serializer, companion, and the extension function in the public API file.Version update:
gradle.propertiesfrom1.21.11-2.70.3to1.21.11-2.71.0to reflect the new feature addition.