Skip to content

Commit a664082

Browse files
grdsdevclaude
andauthored
fix(storage): make dedicated storage host opt-in via useNewHostname flag (#1329)
_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: #1328 Linear: SDK-789 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 70fe768 commit a664082

4 files changed

Lines changed: 134 additions & 44 deletions

File tree

packages/storage_client/lib/src/storage_client.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,25 @@ class SupabaseStorageClient extends StorageBucketApi {
3131
/// 8. 30000 ms +/- 25%
3232
///
3333
/// Anything beyond the 8th try will have 30 second delay.
34+
///
35+
/// [useNewHostname] controls whether legacy storage URLs are rewritten to use
36+
/// the dedicated storage host (`<ref>.storage.supabase.co`). Set to `true`
37+
/// only if your project has the dedicated storage host enabled; otherwise
38+
/// every storage request will fail with an `Invalid Storage request` error.
39+
/// Defaults to `false` (opt-in).
3440
SupabaseStorageClient(
3541
String url,
3642
Map<String, String> headers, {
3743
Client? httpClient,
3844
int retryAttempts = 0,
45+
bool useNewHostname = false,
3946
}) : assert(
4047
retryAttempts >= 0,
4148
'retryAttempts has to be greater than or equal to 0',
4249
),
4350
_defaultRetryAttempts = retryAttempts,
4451
super(
45-
_transformStorageUrl(url),
52+
useNewHostname ? _transformStorageUrl(url) : url,
4653
{...Constants.defaultHeaders, ...headers},
4754
httpClient: httpClient,
4855
) {

packages/storage_client/test/basic_test.dart

Lines changed: 110 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -308,58 +308,128 @@ void main() {
308308
});
309309

310310
group('URL Construction', () {
311-
test('should update legacy prod host to new host', () {
312-
const inputUrl = 'https://blah.supabase.co/storage/v1';
313-
const expectedUrl = 'https://blah.storage.supabase.co/v1';
314-
client = SupabaseStorageClient(inputUrl, {
315-
'Authorization': 'Bearer $supabaseKey',
311+
group('default behavior (useNewHostname: false)', () {
312+
test('should NOT transform legacy prod host by default', () {
313+
const inputUrl = 'https://blah.supabase.co/storage/v1';
314+
client = SupabaseStorageClient(inputUrl, {
315+
'Authorization': 'Bearer $supabaseKey',
316+
});
317+
expect(client.url, inputUrl);
316318
});
317-
expect(client.url, expectedUrl);
318-
});
319319

320-
test('should update legacy staging host to new host', () {
321-
const inputUrl = 'https://blah.supabase.red/storage/v1';
322-
const expectedUrl = 'https://blah.storage.supabase.red/v1';
323-
client = SupabaseStorageClient(inputUrl, {
324-
'Authorization': 'Bearer $supabaseKey',
320+
test('should NOT transform legacy staging host by default', () {
321+
const inputUrl = 'https://blah.supabase.red/storage/v1';
322+
client = SupabaseStorageClient(inputUrl, {
323+
'Authorization': 'Bearer $supabaseKey',
324+
});
325+
expect(client.url, inputUrl);
325326
});
326-
expect(client.url, expectedUrl);
327-
});
328327

329-
test('should accept new host without modification', () {
330-
const inputUrl = 'https://blah.storage.supabase.co/v1';
331-
const expectedUrl = 'https://blah.storage.supabase.co/v1';
332-
client = SupabaseStorageClient(inputUrl, {
333-
'Authorization': 'Bearer $supabaseKey',
328+
test('should NOT transform legacy supabase.in host by default', () {
329+
const inputUrl = 'https://blah.supabase.in/storage/v1';
330+
client = SupabaseStorageClient(inputUrl, {
331+
'Authorization': 'Bearer $supabaseKey',
332+
});
333+
expect(client.url, inputUrl);
334334
});
335-
expect(client.url, expectedUrl);
336-
});
337335

338-
test('should not modify non-platform hosts', () {
339-
const inputUrl = 'https://blah.supabase.co.example.com/storage/v1';
340-
const expectedUrl = 'https://blah.supabase.co.example.com/storage/v1';
341-
client = SupabaseStorageClient(inputUrl, {
342-
'Authorization': 'Bearer $supabaseKey',
336+
test('should accept new host without modification', () {
337+
const inputUrl = 'https://blah.storage.supabase.co/v1';
338+
client = SupabaseStorageClient(inputUrl, {
339+
'Authorization': 'Bearer $supabaseKey',
340+
});
341+
expect(client.url, inputUrl);
343342
});
344-
expect(client.url, expectedUrl);
345-
});
346343

347-
test('should support local host with port without modification', () {
348-
const inputUrl = 'http://localhost:1234/storage/v1';
349-
const expectedUrl = 'http://localhost:1234/storage/v1';
350-
client = SupabaseStorageClient(inputUrl, {
351-
'Authorization': 'Bearer $supabaseKey',
344+
test('should not modify non-platform hosts', () {
345+
const inputUrl = 'https://blah.supabase.co.example.com/storage/v1';
346+
client = SupabaseStorageClient(inputUrl, {
347+
'Authorization': 'Bearer $supabaseKey',
348+
});
349+
expect(client.url, inputUrl);
350+
});
351+
352+
test('should support local host with port without modification', () {
353+
const inputUrl = 'http://localhost:1234/storage/v1';
354+
client = SupabaseStorageClient(inputUrl, {
355+
'Authorization': 'Bearer $supabaseKey',
356+
});
357+
expect(client.url, inputUrl);
352358
});
353-
expect(client.url, expectedUrl);
354359
});
355360

356-
test('should update legacy supabase.in host to new host', () {
357-
const inputUrl = 'https://blah.supabase.in/storage/v1';
358-
const expectedUrl = 'https://blah.storage.supabase.in/v1';
359-
client = SupabaseStorageClient(inputUrl, {
360-
'Authorization': 'Bearer $supabaseKey',
361+
group('opt-in behavior (useNewHostname: true)', () {
362+
test('should update legacy prod host to new host', () {
363+
const inputUrl = 'https://blah.supabase.co/storage/v1';
364+
const expectedUrl = 'https://blah.storage.supabase.co/v1';
365+
client = SupabaseStorageClient(
366+
inputUrl,
367+
{
368+
'Authorization': 'Bearer $supabaseKey',
369+
},
370+
useNewHostname: true);
371+
expect(client.url, expectedUrl);
372+
});
373+
374+
test('should update legacy staging host to new host', () {
375+
const inputUrl = 'https://blah.supabase.red/storage/v1';
376+
const expectedUrl = 'https://blah.storage.supabase.red/v1';
377+
client = SupabaseStorageClient(
378+
inputUrl,
379+
{
380+
'Authorization': 'Bearer $supabaseKey',
381+
},
382+
useNewHostname: true);
383+
expect(client.url, expectedUrl);
384+
});
385+
386+
test('should accept new host without modification', () {
387+
const inputUrl = 'https://blah.storage.supabase.co/v1';
388+
const expectedUrl = 'https://blah.storage.supabase.co/v1';
389+
client = SupabaseStorageClient(
390+
inputUrl,
391+
{
392+
'Authorization': 'Bearer $supabaseKey',
393+
},
394+
useNewHostname: true);
395+
expect(client.url, expectedUrl);
396+
});
397+
398+
test('should not modify non-platform hosts', () {
399+
const inputUrl = 'https://blah.supabase.co.example.com/storage/v1';
400+
const expectedUrl = 'https://blah.supabase.co.example.com/storage/v1';
401+
client = SupabaseStorageClient(
402+
inputUrl,
403+
{
404+
'Authorization': 'Bearer $supabaseKey',
405+
},
406+
useNewHostname: true);
407+
expect(client.url, expectedUrl);
408+
});
409+
410+
test('should support local host with port without modification', () {
411+
const inputUrl = 'http://localhost:1234/storage/v1';
412+
const expectedUrl = 'http://localhost:1234/storage/v1';
413+
client = SupabaseStorageClient(
414+
inputUrl,
415+
{
416+
'Authorization': 'Bearer $supabaseKey',
417+
},
418+
useNewHostname: true);
419+
expect(client.url, expectedUrl);
420+
});
421+
422+
test('should update legacy supabase.in host to new host', () {
423+
const inputUrl = 'https://blah.supabase.in/storage/v1';
424+
const expectedUrl = 'https://blah.storage.supabase.in/v1';
425+
client = SupabaseStorageClient(
426+
inputUrl,
427+
{
428+
'Authorization': 'Bearer $supabaseKey',
429+
},
430+
useNewHostname: true);
431+
expect(client.url, expectedUrl);
361432
});
362-
expect(client.url, expectedUrl);
363433
});
364434
});
365435
}

packages/supabase/lib/src/supabase_client.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ class SupabaseClient {
149149
AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken);
150150
rest = _initRestClient();
151151
functions = _initFunctionsClient();
152-
storage = _initStorageClient(storageOptions.retryAttempts);
152+
storage = _initStorageClient(
153+
storageOptions.retryAttempts, storageOptions.useNewHostname);
153154
realtime = _initRealtimeClient(options: realtimeClientOptions);
154155
if (accessToken == null) {
155156
_log.config(
@@ -316,12 +317,14 @@ class SupabaseClient {
316317
);
317318
}
318319

319-
SupabaseStorageClient _initStorageClient(int storageRetryAttempts) {
320+
SupabaseStorageClient _initStorageClient(
321+
int storageRetryAttempts, bool useNewHostname) {
320322
return SupabaseStorageClient(
321323
_storageUrl,
322324
{...headers},
323325
httpClient: _authHttpClient,
324326
retryAttempts: storageRetryAttempts,
327+
useNewHostname: useNewHostname,
325328
);
326329
}
327330

packages/supabase/lib/src/supabase_client_options.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,17 @@ class AuthClientOptions {
2121
class StorageClientOptions {
2222
final int retryAttempts;
2323

24-
const StorageClientOptions({this.retryAttempts = 0});
24+
/// Whether to rewrite legacy storage URLs to use the dedicated storage host
25+
/// (`<ref>.storage.supabase.co`). Enables uploads larger than 50 GB by
26+
/// bypassing proxy buffering limits.
27+
///
28+
/// Set to `true` only if your project has the dedicated storage host
29+
/// enabled; otherwise every storage request will fail with an
30+
/// `Invalid Storage request` error. Defaults to `false` (opt-in).
31+
final bool useNewHostname;
32+
33+
const StorageClientOptions(
34+
{this.retryAttempts = 0, this.useNewHostname = false});
2535
}
2636

2737
class FunctionsClientOptions {

0 commit comments

Comments
 (0)