This is an automated email from the ASF dual-hosted git repository.
cloud-fan 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 c3b200ed70b9 [SPARK-58629][CONNECT] Avoid empty fallback gRPC errors
c3b200ed70b9 is described below
commit c3b200ed70b904b53d64737812d37e2f9b6f18c8
Author: haoyangeng-db <[email protected]>
AuthorDate: Thu Aug 20 02:23:00 2026 +0800
[SPARK-58629][CONNECT] Avoid empty fallback gRPC errors
### What changes were proposed in this pull request?
This PR makes Spark Connect's fallback gRPC error description non-empty.
When a throwable has a non-empty message, the message remains abbreviated
to 2048 characters. Otherwise, the throwable's fully qualified class name is
used.
A regression test covers a bare `InterruptedException`, which has no
message.
### Why are the changes needed?
Fatal throwables such as `InterruptedException` reach the generic fallback
path. `Utils.abbreviate(null, 2048)` returns `null`, causing Spark Connect to
emit an `UNKNOWN` status without a useful description.
Clients consequently receive an opaque error. This change preserves the
status code while providing the throwable class as useful diagnostic context.
### Does this PR introduce _any_ user-facing change?
Yes. For an already-failing request whose fatal throwable has no message,
the client now receives the throwable class name instead of an empty error
description. Successful requests and other error-conversion paths are unchanged.
### How was this patch tested?
Added a regression test to `FetchErrorDetailsHandlerSuite` verifying that a
bare `InterruptedException` produces an `UNKNOWN` status with
`java.lang.InterruptedException` as its description.
The focused suite could not run locally because the required SBT
dependencies were unavailable in the development environment. Static diff
checks passed; the regression will also run in CI.
### Was this patch authored or co-authored using generative AI tooling?
Co-authord with Codex.
Closes #57837 from haoyangeng-db/spark-connect-empty-fallback-description.
Authored-by: haoyangeng-db <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
(cherry picked from commit f167d371629c0ea5c8ff01a783215d1dc7d96fec)
Signed-off-by: Wenchen Fan <[email protected]>
---
.../spark/sql/connect/utils/ErrorUtils.scala | 11 ++++-
.../spark/sql/connect/utils/ErrorUtilsSuite.scala | 51 ++++++++++++++++++++++
2 files changed, 61 insertions(+), 1 deletion(-)
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 588493aeaa92..01644663e2af 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
@@ -287,6 +287,15 @@ private[connect] object ErrorUtils extends Logging {
.exists(_.toString.contains("org.apache.spark.sql.execution.python"))
}
+ private def fallbackErrorDescription(e: Throwable): String = {
+ val message = e.getMessage
+ if (message != null && message.nonEmpty) {
+ Utils.abbreviate(message, 2048)
+ } else {
+ e.getClass.getName
+ }
+ }
+
/**
* Process an error by retrieving session context, converting to gRPC
status, logging, posting
* events, and executing callbacks. This is the core error handling logic
shared by both
@@ -343,7 +352,7 @@ private[connect] object ErrorUtils extends Logging {
case e: Throwable =>
Status.UNKNOWN
.withCause(e)
- .withDescription(Utils.abbreviate(e.getMessage, 2048))
+ .withDescription(fallbackErrorDescription(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
new file mode 100644
index 000000000000..45cc87e14d09
--- /dev/null
+++
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/utils/ErrorUtilsSuite.scala
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.connect.utils
+
+import java.util.UUID
+
+import io.grpc.{Status, StatusRuntimeException}
+import io.grpc.stub.StreamObserver
+
+import org.apache.spark.sql.test.SharedSparkSession
+
+class ErrorUtilsSuite extends SharedSparkSession {
+
+ test("handleError fallback uses throwable class when fatal throwable has no
message") {
+ val observer = new StreamObserver[Unit] {
+ override def onNext(value: Unit): Unit = {
+ fail(s"Unexpected response: $value")
+ }
+
+ override def onError(t: Throwable): Unit = {
+ throw t
+ }
+
+ override def onCompleted(): Unit = {
+ fail("Unexpected completion")
+ }
+ }
+
+ val error = intercept[StatusRuntimeException] {
+ ErrorUtils.handleError("execute", observer, "user1",
UUID.randomUUID().toString)(
+ new InterruptedException())
+ }
+
+ assert(error.getStatus.getCode == Status.Code.UNKNOWN)
+ assert(error.getStatus.getDescription ==
classOf[InterruptedException].getName)
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]