This is an automated email from the ASF dual-hosted git repository.
zhengruifeng pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new adf0b7ddb6ce [SPARK-58629][CONNECT][FOLLOWUP] Avoid empty INTERNAL
gRPC error messages
adf0b7ddb6ce is described below
commit adf0b7ddb6ce8242e33edc3173e6c6a7ede553bf
Author: haoyangeng-db <[email protected]>
AuthorDate: Thu Aug 20 16:03:29 2026 +0800
[SPARK-58629][CONNECT][FOLLOWUP] Avoid empty INTERNAL gRPC error messages
### What changes were proposed in this pull request?
Follow-up to #57837, which made the fallback (`UNKNOWN`) gRPC error
description non-empty. This applies the same class-name fallback to the
non-fatal `INTERNAL` path.
`ErrorUtils.buildStatusFromThrowable` took its status message from
`SparkConnectService.extractErrorMessage`, which abbreviated `getMessage` and
then mapped `null` to `""`. Since `Utils.abbreviate(null, 2048)` returns
`null`, a non-fatal throwable with no message produced an `INTERNAL` status
with an empty message.
The message now comes from the helper added in #57837, renamed from
`fallbackErrorDescription` to `errorDescription` since it is no longer used
only by the fallback path. `SparkConnectService.extractErrorMessage` and
`convertNullString` are left with no callers and are removed.
### Why are the changes needed?
A message-less non-fatal exception, for example a bare `new
RuntimeException()`, reached the client as an `INTERNAL` error with no
description at all, giving no indication of what failed. This is the same gap
#57837 closed for the fatal path.
### Does this PR introduce _any_ user-facing change?
Yes. For an already-failing request whose non-fatal throwable has no
message, the client now receives the throwable's fully qualified class name
instead of an empty error message. This also covers a throwable whose message
is the empty string, which previously produced an empty message as well.
Successful requests, and errors whose throwable has a non-empty message,
are unchanged.
### How was this patch tested?
Added three cases to `ErrorUtilsSuite` for `buildStatusFromThrowable`: a
throwable with no message and a throwable with an empty message both yield
`java.lang.RuntimeException` as the status message, and a throwable with a
message keeps that message.
The suite could not be run locally because the SBT launcher and Maven
dependencies are unreachable in my development environment, so it runs in CI.
Static checks passed: no remaining reference to the removed helpers anywhere in
the tree, and every touched file's imports are still used.
### Was this patch authored or co-authored using generative AI tooling?
Co-authored with Claude Code
Closes #58134 from haoyangeng-db/spark-connect-internal-error-description.
Authored-by: haoyangeng-db <[email protected]>
Signed-off-by: Ruifeng Zheng <[email protected]>
(cherry picked from commit b9c9451fa68fbe44217dabaf7aed36de1609b17f)
Signed-off-by: Ruifeng Zheng <[email protected]>
---
.../sql/connect/service/SparkConnectService.scala | 13 -------------
.../spark/sql/connect/utils/ErrorUtils.scala | 12 +++++++++---
.../spark/sql/connect/utils/ErrorUtilsSuite.scala | 22 ++++++++++++++++++++++
3 files changed, 31 insertions(+), 16 deletions(-)
diff --git
a/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectService.scala
b/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectService.scala
index 0c18a7491c1f..1fa9fbeda1e5 100644
---
a/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectService.scala
+++
b/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectService.scala
@@ -559,19 +559,6 @@ object SparkConnectService extends Logging {
listenerBus.post(eventBuilder(bindingAddress))
}
-
- def extractErrorMessage(st: Throwable): String = {
- val message = Utils.abbreviate(st.getMessage, 2048)
- convertNullString(message)
- }
-
- def convertNullString(str: String): String = {
- if (str != null) {
- str
- } else {
- ""
- }
- }
}
/**
diff --git
a/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
b/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
index 01644663e2af..0a5a411093b7 100644
---
a/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
+++
b/sql/connect/server/src/main/scala/org/apache/spark/sql/connect/utils/ErrorUtils.scala
@@ -276,7 +276,7 @@ private[connect] object ErrorUtils extends Logging {
.newBuilder()
.setCode(RPCCode.INTERNAL_VALUE)
.addDetails(ProtoAny.pack(withStackTrace.build()))
- .setMessage(SparkConnectService.extractErrorMessage(st))
+ .setMessage(errorDescription(st))
.build()
}
@@ -287,7 +287,13 @@ private[connect] object ErrorUtils extends Logging {
.exists(_.toString.contains("org.apache.spark.sql.execution.python"))
}
- private def fallbackErrorDescription(e: Throwable): String = {
+ /**
+ * Returns a non-empty description for the given throwable: its abbreviated
message when one is
+ * present, and its fully qualified class name otherwise. Both the INTERNAL
status built for a
+ * non-fatal throwable and the UNKNOWN fallback status use this, so that a
client never receives
+ * an error whose description is empty.
+ */
+ private def errorDescription(e: Throwable): String = {
val message = e.getMessage
if (message != null && message.nonEmpty) {
Utils.abbreviate(message, 2048)
@@ -352,7 +358,7 @@ private[connect] object ErrorUtils extends Logging {
case e: Throwable =>
Status.UNKNOWN
.withCause(e)
- .withDescription(fallbackErrorDescription(e))
+ .withDescription(errorDescription(e))
.asRuntimeException()
}
diff --git
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
index 45cc87e14d09..97b9c8db673b 100644
---
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
+++
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
@@ -18,6 +18,7 @@ package org.apache.spark.sql.connect.utils
import java.util.UUID
+import com.google.rpc.Code
import io.grpc.{Status, StatusRuntimeException}
import io.grpc.stub.StreamObserver
@@ -48,4 +49,25 @@ class ErrorUtilsSuite extends SharedSparkSession {
assert(error.getStatus.getCode == Status.Code.UNKNOWN)
assert(error.getStatus.getDescription ==
classOf[InterruptedException].getName)
}
+
+ test("buildStatusFromThrowable uses throwable class when non-fatal throwable
has no message") {
+ val status = ErrorUtils.buildStatusFromThrowable(new RuntimeException(),
None)
+
+ assert(status.getCode == Code.INTERNAL_VALUE)
+ assert(status.getMessage == classOf[RuntimeException].getName)
+ }
+
+ test("buildStatusFromThrowable uses throwable class when non-fatal message
is empty") {
+ val status = ErrorUtils.buildStatusFromThrowable(new RuntimeException(""),
None)
+
+ assert(status.getCode == Code.INTERNAL_VALUE)
+ assert(status.getMessage == classOf[RuntimeException].getName)
+ }
+
+ test("buildStatusFromThrowable keeps the message when the non-fatal
throwable has one") {
+ val status = ErrorUtils.buildStatusFromThrowable(new
RuntimeException("boom"), None)
+
+ assert(status.getCode == Code.INTERNAL_VALUE)
+ assert(status.getMessage == "boom")
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]