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]