From 7ca57cfb7dd1beea65b6af730a1dbca4b61fd103 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 25 Mar 2026 08:31:48 -0300 Subject: [PATCH] fix(storage): make dedicated storage host opt-in via useNewHostname flag _transformStorageUrl unconditionally rewrote all storage URLs to use the dedicated storage host (*.storage.supabase.co), which is not available on all projects. This silently broke every storage operation with StorageException: Invalid Storage request. Option B: change the default to opt-in (useNewHostname defaults to false). Projects that have the dedicated storage host enabled can opt in via StorageClientOptions(useNewHostname: true) in Supabase.initialize(). The URL transformation logic and its tests are preserved; they now run only when useNewHostname: true is passed. Fixes: https://github.com/supabase/supabase-flutter/issues/1328 Linear: SDK-789 Co-Authored-By: Claude Sonnet 4.6 --- .../lib/src/storage_client.dart | 9 +- packages/storage_client/test/basic_test.dart | 150 +++++++++++++----- .../supabase/lib/src/supabase_client.dart | 7 +- .../lib/src/supabase_client_options.dart | 12 +- 4 files changed, 134 insertions(+), 44 deletions(-) diff --git a/packages/storage_client/lib/src/storage_client.dart b/packages/storage_client/lib/src/storage_client.dart index 3e25a70ab..825baff36 100644 --- a/packages/storage_client/lib/src/storage_client.dart +++ b/packages/storage_client/lib/src/storage_client.dart @@ -31,18 +31,25 @@ class SupabaseStorageClient extends StorageBucketApi { /// 8. 30000 ms +/- 25% /// /// Anything beyond the 8th try will have 30 second delay. + /// + /// [useNewHostname] controls whether legacy storage URLs are rewritten to use + /// the dedicated storage host (`.storage.supabase.co`). Set to `true` + /// only if your project has the dedicated storage host enabled; otherwise + /// every storage request will fail with an `Invalid Storage request` error. + /// Defaults to `false` (opt-in). SupabaseStorageClient( String url, Map headers, { Client? httpClient, int retryAttempts = 0, + bool useNewHostname = false, }) : assert( retryAttempts >= 0, 'retryAttempts has to be greater than or equal to 0', ), _defaultRetryAttempts = retryAttempts, super( - _transformStorageUrl(url), + useNewHostname ? _transformStorageUrl(url) : url, {...Constants.defaultHeaders, ...headers}, httpClient: httpClient, ) { diff --git a/packages/storage_client/test/basic_test.dart b/packages/storage_client/test/basic_test.dart index 254ea1d40..168606ede 100644 --- a/packages/storage_client/test/basic_test.dart +++ b/packages/storage_client/test/basic_test.dart @@ -308,58 +308,128 @@ void main() { }); group('URL Construction', () { - test('should update legacy prod host to new host', () { - const inputUrl = 'https://blah.supabase.co/storage/v1'; - const expectedUrl = 'https://blah.storage.supabase.co/v1'; - client = SupabaseStorageClient(inputUrl, { - 'Authorization': 'Bearer $supabaseKey', + group('default behavior (useNewHostname: false)', () { + test('should NOT transform legacy prod host by default', () { + const inputUrl = 'https://blah.supabase.co/storage/v1'; + client = SupabaseStorageClient(inputUrl, { + 'Authorization': 'Bearer $supabaseKey', + }); + expect(client.url, inputUrl); }); - expect(client.url, expectedUrl); - }); - test('should update legacy staging host to new host', () { - const inputUrl = 'https://blah.supabase.red/storage/v1'; - const expectedUrl = 'https://blah.storage.supabase.red/v1'; - client = SupabaseStorageClient(inputUrl, { - 'Authorization': 'Bearer $supabaseKey', + test('should NOT transform legacy staging host by default', () { + const inputUrl = 'https://blah.supabase.red/storage/v1'; + client = SupabaseStorageClient(inputUrl, { + 'Authorization': 'Bearer $supabaseKey', + }); + expect(client.url, inputUrl); }); - expect(client.url, expectedUrl); - }); - test('should accept new host without modification', () { - const inputUrl = 'https://blah.storage.supabase.co/v1'; - const expectedUrl = 'https://blah.storage.supabase.co/v1'; - client = SupabaseStorageClient(inputUrl, { - 'Authorization': 'Bearer $supabaseKey', + test('should NOT transform legacy supabase.in host by default', () { + const inputUrl = 'https://blah.supabase.in/storage/v1'; + client = SupabaseStorageClient(inputUrl, { + 'Authorization': 'Bearer $supabaseKey', + }); + expect(client.url, inputUrl); }); - expect(client.url, expectedUrl); - }); - test('should not modify non-platform hosts', () { - const inputUrl = 'https://blah.supabase.co.example.com/storage/v1'; - const expectedUrl = 'https://blah.supabase.co.example.com/storage/v1'; - client = SupabaseStorageClient(inputUrl, { - 'Authorization': 'Bearer $supabaseKey', + test('should accept new host without modification', () { + const inputUrl = 'https://blah.storage.supabase.co/v1'; + client = SupabaseStorageClient(inputUrl, { + 'Authorization': 'Bearer $supabaseKey', + }); + expect(client.url, inputUrl); }); - expect(client.url, expectedUrl); - }); - test('should support local host with port without modification', () { - const inputUrl = 'http://localhost:1234/storage/v1'; - const expectedUrl = 'http://localhost:1234/storage/v1'; - client = SupabaseStorageClient(inputUrl, { - 'Authorization': 'Bearer $supabaseKey', + test('should not modify non-platform hosts', () { + const inputUrl = 'https://blah.supabase.co.example.com/storage/v1'; + client = SupabaseStorageClient(inputUrl, { + 'Authorization': 'Bearer $supabaseKey', + }); + expect(client.url, inputUrl); + }); + + test('should support local host with port without modification', () { + const inputUrl = 'http://localhost:1234/storage/v1'; + client = SupabaseStorageClient(inputUrl, { + 'Authorization': 'Bearer $supabaseKey', + }); + expect(client.url, inputUrl); }); - expect(client.url, expectedUrl); }); - test('should update legacy supabase.in host to new host', () { - const inputUrl = 'https://blah.supabase.in/storage/v1'; - const expectedUrl = 'https://blah.storage.supabase.in/v1'; - client = SupabaseStorageClient(inputUrl, { - 'Authorization': 'Bearer $supabaseKey', + group('opt-in behavior (useNewHostname: true)', () { + test('should update legacy prod host to new host', () { + const inputUrl = 'https://blah.supabase.co/storage/v1'; + const expectedUrl = 'https://blah.storage.supabase.co/v1'; + client = SupabaseStorageClient( + inputUrl, + { + 'Authorization': 'Bearer $supabaseKey', + }, + useNewHostname: true); + expect(client.url, expectedUrl); + }); + + test('should update legacy staging host to new host', () { + const inputUrl = 'https://blah.supabase.red/storage/v1'; + const expectedUrl = 'https://blah.storage.supabase.red/v1'; + client = SupabaseStorageClient( + inputUrl, + { + 'Authorization': 'Bearer $supabaseKey', + }, + useNewHostname: true); + expect(client.url, expectedUrl); + }); + + test('should accept new host without modification', () { + const inputUrl = 'https://blah.storage.supabase.co/v1'; + const expectedUrl = 'https://blah.storage.supabase.co/v1'; + client = SupabaseStorageClient( + inputUrl, + { + 'Authorization': 'Bearer $supabaseKey', + }, + useNewHostname: true); + expect(client.url, expectedUrl); + }); + + test('should not modify non-platform hosts', () { + const inputUrl = 'https://blah.supabase.co.example.com/storage/v1'; + const expectedUrl = 'https://blah.supabase.co.example.com/storage/v1'; + client = SupabaseStorageClient( + inputUrl, + { + 'Authorization': 'Bearer $supabaseKey', + }, + useNewHostname: true); + expect(client.url, expectedUrl); + }); + + test('should support local host with port without modification', () { + const inputUrl = 'http://localhost:1234/storage/v1'; + const expectedUrl = 'http://localhost:1234/storage/v1'; + client = SupabaseStorageClient( + inputUrl, + { + 'Authorization': 'Bearer $supabaseKey', + }, + useNewHostname: true); + expect(client.url, expectedUrl); + }); + + test('should update legacy supabase.in host to new host', () { + const inputUrl = 'https://blah.supabase.in/storage/v1'; + const expectedUrl = 'https://blah.storage.supabase.in/v1'; + client = SupabaseStorageClient( + inputUrl, + { + 'Authorization': 'Bearer $supabaseKey', + }, + useNewHostname: true); + expect(client.url, expectedUrl); }); - expect(client.url, expectedUrl); }); }); } diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 2bd00aac5..f9f124820 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -149,7 +149,8 @@ class SupabaseClient { AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken); rest = _initRestClient(); functions = _initFunctionsClient(); - storage = _initStorageClient(storageOptions.retryAttempts); + storage = _initStorageClient( + storageOptions.retryAttempts, storageOptions.useNewHostname); realtime = _initRealtimeClient(options: realtimeClientOptions); if (accessToken == null) { _log.config( @@ -316,12 +317,14 @@ class SupabaseClient { ); } - SupabaseStorageClient _initStorageClient(int storageRetryAttempts) { + SupabaseStorageClient _initStorageClient( + int storageRetryAttempts, bool useNewHostname) { return SupabaseStorageClient( _storageUrl, {...headers}, httpClient: _authHttpClient, retryAttempts: storageRetryAttempts, + useNewHostname: useNewHostname, ); } diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart index 0c0c404fb..12045bed6 100644 --- a/packages/supabase/lib/src/supabase_client_options.dart +++ b/packages/supabase/lib/src/supabase_client_options.dart @@ -21,7 +21,17 @@ class AuthClientOptions { class StorageClientOptions { final int retryAttempts; - const StorageClientOptions({this.retryAttempts = 0}); + /// Whether to rewrite legacy storage URLs to use the dedicated storage host + /// (`.storage.supabase.co`). Enables uploads larger than 50 GB by + /// bypassing proxy buffering limits. + /// + /// Set to `true` only if your project has the dedicated storage host + /// enabled; otherwise every storage request will fail with an + /// `Invalid Storage request` error. Defaults to `false` (opt-in). + final bool useNewHostname; + + const StorageClientOptions( + {this.retryAttempts = 0, this.useNewHostname = false}); } class FunctionsClientOptions {