From e554e79d5378836e5b652d852292057c7940c1f3 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 31 Jul 2025 13:16:22 -0300 Subject: [PATCH 1/5] fix(storage): use dedicated storage host --- Sources/Storage/StorageApi.swift | 19 ++++++ Sources/Storage/SupabaseStorage.swift | 7 +- .../StorageTests/StorageBucketAPITests.swift | 64 +++++++++++++++++-- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index a537b6ecf..f3eb3403b 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -15,6 +15,25 @@ public class StorageApi: @unchecked Sendable { if configuration.headers["X-Client-Info"] == nil { configuration.headers["X-Client-Info"] = "storage-swift/\(version)" } + + // if legacy uri is used, replace with new storage host (disables request buffering to allow > 50GB uploads) + // "project-ref.supabase.co" becomes "project-ref.storage.supabase.co" + if configuration.useNewHostname == true { + var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: false)! + let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$") + + let host = components.host! + + let isSupabaseHost = + regex.firstMatch(in: host, range: NSRange(location: 0, length: host.utf16.count)) != nil + + if isSupabaseHost, !host.contains("storage.supabase.") { + components.host = host.replacingOccurrences(of: "supabase.", with: "storage.supabase.") + } + + configuration.url = components.url! + } + self.configuration = configuration var interceptors: [any HTTPClientInterceptor] = [] diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index f4ed85f6c..192564d41 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -1,12 +1,13 @@ import Foundation public struct StorageClientConfiguration: Sendable { - public let url: URL + public var url: URL public var headers: [String: String] public let encoder: JSONEncoder public let decoder: JSONDecoder public let session: StorageHTTPSession public let logger: (any SupabaseLogger)? + public let useNewHostname: Bool? public init( url: URL, @@ -14,7 +15,8 @@ public struct StorageClientConfiguration: Sendable { encoder: JSONEncoder = .defaultStorageEncoder, decoder: JSONDecoder = .defaultStorageDecoder, session: StorageHTTPSession = .init(), - logger: (any SupabaseLogger)? = nil + logger: (any SupabaseLogger)? = nil, + useNewHostname: Bool? = nil ) { self.url = url self.headers = headers @@ -22,6 +24,7 @@ public struct StorageClientConfiguration: Sendable { self.decoder = decoder self.session = session self.logger = logger + self.useNewHostname = useNewHostname } } diff --git a/Tests/StorageTests/StorageBucketAPITests.swift b/Tests/StorageTests/StorageBucketAPITests.swift index 7f8fcd7a0..b519667f9 100644 --- a/Tests/StorageTests/StorageBucketAPITests.swift +++ b/Tests/StorageTests/StorageBucketAPITests.swift @@ -3,12 +3,12 @@ import Mocker import TestHelpers import XCTest +@testable import Storage + #if canImport(FoundationNetworking) import FoundationNetworking #endif -@testable import Storage - final class StorageBucketAPITests: XCTestCase { let url = URL(string: "http://localhost:54321/storage/v1")! var storage: SupabaseStorageClient! @@ -47,6 +47,60 @@ final class StorageBucketAPITests: XCTestCase { Mocker.removeAll() } + func testURLConstruction() { + let urlTestCases = [ + ( + "https://blah.supabase.co/storage/v1", + "https://blah.storage.supabase.co/storage/v1", + "update legacy prod host to new host" + ), + ( + "https://blah.supabase.red/storage/v1", + "https://blah.storage.supabase.red/storage/v1", + "update legacy staging host to new host" + ), + ( + "https://blah.storage.supabase.co/storage/v1", + "https://blah.storage.supabase.co/storage/v1", + "accept new host without modification" + ), + ( + "https://blah.supabase.co.example.com/storage/v1", + "https://blah.supabase.co.example.com/storage/v1", + "not modify non-platform hosts" + ), + ( + "http://localhost:1234/storage/v1", + "http://localhost:1234/storage/v1", + "support local host with port without modification" + ), + ] + + for (input, expect, description) in urlTestCases { + XCTContext.runActivity(named: "should \(description) if ueNewHostname is true") { _ in + let storage = SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: input)!, + headers: [:], + useNewHostname: true + ) + ) + XCTAssertEqual(storage.configuration.url.absoluteString, expect) + } + + XCTContext.runActivity(named: "should not modify host if ueNewHostname is false") { _ in + let storage = SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: input)!, + headers: [:], + useNewHostname: false + ) + ) + XCTAssertEqual(storage.configuration.url.absoluteString, input) + } + } + } + func testGetBucket() async throws { Mock( url: url.appendingPathComponent("bucket/bucket123"), @@ -132,7 +186,8 @@ final class StorageBucketAPITests: XCTestCase { "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z" } - """.utf8) + """.utf8 + ) ] ) .snapshotRequest { @@ -171,7 +226,8 @@ final class StorageBucketAPITests: XCTestCase { "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z" } - """.utf8) + """.utf8 + ) ] ) .snapshotRequest { From cf5d6e2479b75ebaea2e2046c06ba6c43c3ac189 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 31 Jul 2025 13:22:24 -0300 Subject: [PATCH 2/5] fix: expose useNewHostname --- Sources/Supabase/SupabaseClient.swift | 3 ++- Sources/Supabase/Types.swift | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 97b38b2f6..b419a94e8 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -58,7 +58,8 @@ public final class SupabaseClient: Sendable { url: storageURL, headers: headers, session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth), - logger: options.global.logger + logger: options.global.logger, + useNewHostname: options.storage.useNewHostname ) ) } diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 5d5bd9fb0..7534b6904 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -10,6 +10,7 @@ public struct SupabaseClientOptions: Sendable { public let global: GlobalOptions public let functions: FunctionsOptions public let realtime: RealtimeClientOptions + public let storage: StorageOptions public struct DatabaseOptions: Sendable { /// The Postgres schema which your tables belong to. Must be on the list of exposed schemas in @@ -118,18 +119,28 @@ public struct SupabaseClientOptions: Sendable { } } + public struct StorageOptions: Sendable { + public let useNewHostname: Bool? + + public init(useNewHostname: Bool? = nil) { + self.useNewHostname = useNewHostname + } + } + public init( db: DatabaseOptions = .init(), auth: AuthOptions, global: GlobalOptions = .init(), functions: FunctionsOptions = .init(), - realtime: RealtimeClientOptions = .init() + realtime: RealtimeClientOptions = .init(), + storage: StorageOptions = .init() ) { self.db = db self.auth = auth self.global = global self.functions = functions self.realtime = realtime + self.storage = storage } } @@ -139,13 +150,15 @@ extension SupabaseClientOptions { db: DatabaseOptions = .init(), global: GlobalOptions = .init(), functions: FunctionsOptions = .init(), - realtime: RealtimeClientOptions = .init() + realtime: RealtimeClientOptions = .init(), + storage: StorageOptions = .init() ) { self.db = db auth = .init() self.global = global self.functions = functions self.realtime = realtime + self.storage = storage } #endif } From 19be994f622386a34f441f4be10a79ca1b907887 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 31 Jul 2025 13:39:05 -0300 Subject: [PATCH 3/5] fix: make useNewHostname non-null and default to false --- Sources/Storage/StorageApi.swift | 10 +++++++--- Sources/Storage/SupabaseStorage.swift | 4 ++-- Sources/Supabase/Types.swift | 7 ++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index f3eb3403b..c3f3ac422 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -19,10 +19,14 @@ public class StorageApi: @unchecked Sendable { // if legacy uri is used, replace with new storage host (disables request buffering to allow > 50GB uploads) // "project-ref.supabase.co" becomes "project-ref.storage.supabase.co" if configuration.useNewHostname == true { - var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: false)! - let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$") + guard + var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: false), + let host = components.host + else { + fatalError("Client initialized with invalid URL: \(configuration.url)") + } - let host = components.host! + let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$") let isSupabaseHost = regex.firstMatch(in: host, range: NSRange(location: 0, length: host.utf16.count)) != nil diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index 192564d41..ba043c8b8 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -7,7 +7,7 @@ public struct StorageClientConfiguration: Sendable { public let decoder: JSONDecoder public let session: StorageHTTPSession public let logger: (any SupabaseLogger)? - public let useNewHostname: Bool? + public let useNewHostname: Bool public init( url: URL, @@ -16,7 +16,7 @@ public struct StorageClientConfiguration: Sendable { decoder: JSONDecoder = .defaultStorageDecoder, session: StorageHTTPSession = .init(), logger: (any SupabaseLogger)? = nil, - useNewHostname: Bool? = nil + useNewHostname: Bool = false ) { self.url = url self.headers = headers diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 7534b6904..b567d7d34 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -120,9 +120,10 @@ public struct SupabaseClientOptions: Sendable { } public struct StorageOptions: Sendable { - public let useNewHostname: Bool? - - public init(useNewHostname: Bool? = nil) { + /// Whether storage client should be initialized with the new hostname format, i.e. `project-ref.storage.supabase.co` + public let useNewHostname: Bool + + public init(useNewHostname: Bool = false) { self.useNewHostname = useNewHostname } } From 965dc08beead327d87b30a49e2e5539324e0bc6b Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 31 Jul 2025 13:39:31 -0300 Subject: [PATCH 4/5] Update Tests/StorageTests/StorageBucketAPITests.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Tests/StorageTests/StorageBucketAPITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/StorageTests/StorageBucketAPITests.swift b/Tests/StorageTests/StorageBucketAPITests.swift index b519667f9..0da1319fb 100644 --- a/Tests/StorageTests/StorageBucketAPITests.swift +++ b/Tests/StorageTests/StorageBucketAPITests.swift @@ -77,7 +77,7 @@ final class StorageBucketAPITests: XCTestCase { ] for (input, expect, description) in urlTestCases { - XCTContext.runActivity(named: "should \(description) if ueNewHostname is true") { _ in + XCTContext.runActivity(named: "should \(description) if useNewHostname is true") { _ in let storage = SupabaseStorageClient( configuration: StorageClientConfiguration( url: URL(string: input)!, From cef40fb9d6f13509b7ea5809b271fc43c309d36d Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 31 Jul 2025 13:39:38 -0300 Subject: [PATCH 5/5] Update Tests/StorageTests/StorageBucketAPITests.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Tests/StorageTests/StorageBucketAPITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/StorageTests/StorageBucketAPITests.swift b/Tests/StorageTests/StorageBucketAPITests.swift index 0da1319fb..d4de1cd4f 100644 --- a/Tests/StorageTests/StorageBucketAPITests.swift +++ b/Tests/StorageTests/StorageBucketAPITests.swift @@ -88,7 +88,7 @@ final class StorageBucketAPITests: XCTestCase { XCTAssertEqual(storage.configuration.url.absoluteString, expect) } - XCTContext.runActivity(named: "should not modify host if ueNewHostname is false") { _ in + XCTContext.runActivity(named: "should not modify host if useNewHostname is false") { _ in let storage = SupabaseStorageClient( configuration: StorageClientConfiguration( url: URL(string: input)!,