This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-swift.git


The following commit(s) were added to refs/heads/main by this push:
     new fdb2b6d  [SPARK-57912] Improve connection string handling
fdb2b6d is described below

commit fdb2b6d940c2f34f9c3e9b7a5fe5edd7c112d422
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Thu Jul 2 15:33:32 2026 -0700

    [SPARK-57912] Improve connection string handling
    
    ### What changes were proposed in this pull request?
    
    This PR improves the connection string handling of `SparkConnectClient`.
    
    1. `init(remote:)` throws `SparkConnectError.InvalidArgument` instead of 
crashing on an invalid URL, a non-`sc` scheme, or a parameter without a value 
(e.g., `sc://host:15002/;token=`).
    2. Parameter values are split with `maxSplits: 1`, so values containing `=` 
(e.g., `token=abcd==`) are no longer truncated.
    3. The `Unknown parameter` warning prints only the key to prevent 
accidental credential exposure.
    
    ### Why are the changes needed?
    
    Invalid connection strings should surface as catchable errors instead of 
crashes, and credentials should not leak into logs or be silently truncated.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, only for invalid inputs. Previously the process crashed; now 
`SparkConnectError.InvalidArgument` is thrown.
    
    ### How was this patch tested?
    
    Pass the CIs with the newly added test cases.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Fable 5
    
    Closes #429 from dongjoon-hyun/SPARK-57912.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 Sources/SparkConnect/SparkConnectClient.swift      | 19 +++++---
 Sources/SparkConnect/SparkSession.swift            |  6 +--
 Tests/SparkConnectTests/RuntimeConfTests.swift     | 14 +++---
 .../SparkConnectClientTests.swift                  | 51 +++++++++++++++++-----
 4 files changed, 63 insertions(+), 27 deletions(-)

diff --git a/Sources/SparkConnect/SparkConnectClient.swift 
b/Sources/SparkConnect/SparkConnectClient.swift
index a68a465..cb67026 100644
--- a/Sources/SparkConnect/SparkConnectClient.swift
+++ b/Sources/SparkConnect/SparkConnectClient.swift
@@ -42,8 +42,13 @@ public actor SparkConnectClient {
   /// Create a client to use GRPCClient.
   /// - Parameters:
   ///   - remote: A string to connect `Spark Connect` server.
-  init(remote: String) {
-    self.url = URL(string: remote)!
+  /// - Throws: `SparkConnectError.InvalidArgument` if `remote` is not a valid 
`sc://` connection
+  /// string or a parameter has no value.
+  init(remote: String) throws {
+    guard let url = URL(string: remote), url.scheme == "sc" else {
+      throw SparkConnectError.InvalidArgument
+    }
+    self.url = url
     self.host = url.host() ?? "localhost"
     self.port = self.url.port ?? 15002
     var token: String? = nil
@@ -54,7 +59,10 @@ public actor SparkConnectClient {
       var userName = processInfo.environment["SPARK_USER"] ?? ""
     #endif
     for param in self.url.path.split(separator: ";").dropFirst().filter({ 
!$0.isEmpty }) {
-      let kv = param.split(separator: "=")
+      let kv = param.split(separator: "=", maxSplits: 1)
+      guard kv.count == 2 else {
+        throw SparkConnectError.InvalidArgument
+      }
       switch String(kv[0]).lowercased() {
       case URIParams.PARAM_SESSION_ID:
         // SparkSession handles this.
@@ -70,8 +78,9 @@ public actor SparkConnectClient {
           self.useTLS = true
         }
       default:
-        // Print warning and ignore
-        print("Unknown parameter: \(param)")
+        // Print warning and ignore. Note that the parameter value is not 
printed
+        // in order to prevent accidental credential exposure.
+        print("Unknown parameter: \(kv[0])")
       }
     }
     self.token = token ?? 
ProcessInfo.processInfo.environment["SPARK_CONNECT_AUTHENTICATE_TOKEN"]
diff --git a/Sources/SparkConnect/SparkSession.swift 
b/Sources/SparkConnect/SparkSession.swift
index 66c12aa..5a007f6 100644
--- a/Sources/SparkConnect/SparkSession.swift
+++ b/Sources/SparkConnect/SparkSession.swift
@@ -44,8 +44,8 @@ public actor SparkSession {
   /// Create a session that uses the specified connection string and userID.
   /// - Parameters:
   ///   - connection: a string in a patter, `sc://{host}:{port}`
-  init(_ connection: String) {
-    self.client = SparkConnectClient(remote: connection)
+  init(_ connection: String) throws {
+    self.client = try SparkConnectClient(remote: connection)
     // Since `Session ID` belongs to `SparkSession`, we handle this here.
     if connection.contains(regexSessionID) {
       self.sessionID = connection.firstMatch(of: 
regexSessionID)!.1.lowercased()
@@ -604,7 +604,7 @@ public actor SparkSession {
     /// - Returns: A newly created ``SparkSession``.
     func create() async throws -> SparkSession {
       let remote = ProcessInfo.processInfo.environment["SPARK_REMOTE"] ?? 
"sc://localhost:15002"
-      let session = SparkSession(sparkConf["spark.remote"] ?? remote)
+      let session = try SparkSession(sparkConf["spark.remote"] ?? remote)
       let response = try await session.client.connect(session.sessionID)
       await session.setVersion(response.sparkVersion.version)
       let isSuccess = try await session.client.setConf(map: sparkConf)
diff --git a/Tests/SparkConnectTests/RuntimeConfTests.swift 
b/Tests/SparkConnectTests/RuntimeConfTests.swift
index d7abafb..425be88 100644
--- a/Tests/SparkConnectTests/RuntimeConfTests.swift
+++ b/Tests/SparkConnectTests/RuntimeConfTests.swift
@@ -33,7 +33,7 @@ struct RuntimeConfTests {
 
   @Test
   func get() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
 
@@ -48,7 +48,7 @@ struct RuntimeConfTests {
 
   @Test
   func getWithDefault() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
     #expect(try await conf.get("spark.sql.adaptive.customCostEvaluatorClass", 
"XYZ") == "XYZ")
@@ -58,7 +58,7 @@ struct RuntimeConfTests {
 
   @Test
   func getOption() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
     #expect(try await conf.getOption("spark.app.name") != nil)
@@ -68,7 +68,7 @@ struct RuntimeConfTests {
 
   @Test
   func set() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
     try await conf.set("spark.test.key1", "value1")
@@ -78,7 +78,7 @@ struct RuntimeConfTests {
 
   @Test
   func reset() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
 
@@ -99,7 +99,7 @@ struct RuntimeConfTests {
 
   @Test
   func getAll() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
     let map = try await conf.getAll()
@@ -113,7 +113,7 @@ struct RuntimeConfTests {
 
   @Test
   func isModifiable() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let conf = RuntimeConf(client)
     #expect(try await 
conf.isModifiable("spark.sql.adaptive.customCostEvaluatorClass"))
diff --git a/Tests/SparkConnectTests/SparkConnectClientTests.swift 
b/Tests/SparkConnectTests/SparkConnectClientTests.swift
index 639d306..4fac0e5 100644
--- a/Tests/SparkConnectTests/SparkConnectClientTests.swift
+++ b/Tests/SparkConnectTests/SparkConnectClientTests.swift
@@ -33,13 +33,13 @@ struct SparkConnectClientTests {
 
   @Test
   func createAndStop() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     await client.stop()
   }
 
   @Test
   func parameters() async throws {
-    let client = SparkConnectClient(
+    let client = try SparkConnectClient(
       remote: "sc://host1:123/;tOkeN=abcd;user_ID=test;USER_agent=myagent")
     #expect(await client.token == "abcd")
     #expect(await client.userContext.userID == "test")
@@ -49,9 +49,36 @@ struct SparkConnectClientTests {
     await client.stop()
   }
 
+  @Test
+  func parameterWithBase64Token() async throws {
+    let client = try SparkConnectClient(remote: "sc://host1:123/;token=abcd==")
+    #expect(await client.token == "abcd==")
+    await client.stop()
+  }
+
+  @Test
+  func invalidRemote() async throws {
+    #expect(throws: SparkConnectError.InvalidArgument) {
+      try SparkConnectClient(remote: "http://host1:123";)
+    }
+    #expect(throws: SparkConnectError.InvalidArgument) {
+      try SparkConnectClient(remote: "host1:123")
+    }
+  }
+
+  @Test
+  func parameterWithoutValue() async throws {
+    #expect(throws: SparkConnectError.InvalidArgument) {
+      try SparkConnectClient(remote: "sc://host1:123/;token=")
+    }
+    #expect(throws: SparkConnectError.InvalidArgument) {
+      try SparkConnectClient(remote: "sc://host1:123/;use_ssl")
+    }
+  }
+
   @Test
   func connectWithInvalidUUID() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await #require(throws: SparkConnectError.InvalidSessionID) {
       try await client.connect("not-a-uuid-format")
     }
@@ -60,14 +87,14 @@ struct SparkConnectClientTests {
 
   @Test
   func connect() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     await client.stop()
   }
 
   @Test
   func tags() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     let plan = await client.getPlanRange(0, 1, 1)
 
@@ -83,7 +110,7 @@ struct SparkConnectClientTests {
 
   @Test
   func ddlParse() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     try await client.connect(UUID().uuidString)
     #expect(try await client.ddlParse("a int").simpleString == "struct<a:int>")
     await client.stop()
@@ -91,7 +118,7 @@ struct SparkConnectClientTests {
 
   @Test
   func jsonToDdl() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     let response = try await client.connect(UUID().uuidString)
     if response.sparkVersion.version.starts(with: "4.") {
       let json =
@@ -103,7 +130,7 @@ struct SparkConnectClientTests {
 
   @Test
   func createDataflowGraph() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     let response = try await client.connect(UUID().uuidString)
     if response.sparkVersion.version.starts(with: "4.1") {
       let dataflowGraphID = try await client.createDataflowGraph()
@@ -114,7 +141,7 @@ struct SparkConnectClientTests {
 
   // @Test
   func startRun() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     let response = try await client.connect(UUID().uuidString)
 
     try await #require(throws: SparkConnectError.InvalidArgument) {
@@ -131,7 +158,7 @@ struct SparkConnectClientTests {
 
   // @Test
   func defineOutput() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     let response = try await client.connect(UUID().uuidString)
 
     try await #require(throws: SparkConnectError.InvalidArgument) {
@@ -153,7 +180,7 @@ struct SparkConnectClientTests {
 
   // @Test
   func defineFlow() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     let response = try await client.connect(UUID().uuidString)
 
     try await #require(throws: SparkConnectError.InvalidArgument) {
@@ -171,7 +198,7 @@ struct SparkConnectClientTests {
 
   @Test
   func defineSqlGraphElements() async throws {
-    let client = SparkConnectClient(remote: TEST_REMOTE)
+    let client = try SparkConnectClient(remote: TEST_REMOTE)
     let response = try await client.connect(UUID().uuidString)
 
     try await #require(throws: SparkConnectError.InvalidArgument) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to