Skip to content

Add SerializableError class for structured error serialization#263

Merged
twisti-dev merged 1 commit intoversion/1.21.11from
feat/serializable-error
Mar 26, 2026
Merged

Add SerializableError class for structured error serialization#263
twisti-dev merged 1 commit intoversion/1.21.11from
feat/serializable-error

Conversation

@twisti-dev
Copy link
Copy Markdown
Contributor

This pull request introduces a new utility for serializing errors in a structured way. The main change is the addition of a SerializableError data 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:

  • Added a new SerializableError data class in SerializableError.kt that represents errors in a serializable structure, including type, message, and optional nested cause. This class also provides a method to reconstruct a Throwable from its serialized form.
  • Implemented a top-level extension function toSerializableError() to convert any Throwable into a SerializableError, recursively serializing causes.
  • Generated the corresponding API signatures for SerializableError, its serializer, companion, and the extension function in the public API file.

Version update:

  • Bumped the project version in gradle.properties from 1.21.11-2.70.3 to 1.21.11-2.71.0 to reflect the new feature addition.

@twisti-dev twisti-dev self-assigned this Mar 26, 2026
Copilot AI review requested due to automatic review settings March 26, 2026 16:29
@twisti-dev twisti-dev merged commit b23479f into version/1.21.11 Mar 26, 2026
2 of 3 checks passed
@twisti-dev twisti-dev deleted the feat/serializable-error branch March 26, 2026 16:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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) into SerializableError.
  • Bumped project version to 1.21.11-2.71.0 and 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())
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
return RuntimeException(throwableMessage, cause?.buildFakeThrowable())
return RuntimeException(throwableMessage, cause?.buildFakeThrowable(), false, false)

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +42

return RuntimeException(throwableMessage, cause?.buildFakeThrowable())
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +57
* @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
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
* @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
)
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants