Skip to content

Commit 9923153

Browse files
grdsdevclaude
andauthored
feat(storage): add setHeader method for custom HTTP headers (#1313)
Add setHeader(key, value) method to both SupabaseStorageClient and StorageFileApi to allow setting per-request HTTP headers on storage operations, matching the supabase-js API. Key behaviors: - Creates shallow copy of headers to avoid mutating shared state - Returns this for method chaining - StorageFileApi instances maintain isolated headers Linear: SDK-691 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bfa480a commit 9923153

File tree

3 files changed

+162
-4
lines changed

3 files changed

+162
-4
lines changed

packages/storage_client/lib/src/storage_client.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,17 @@ class SupabaseStorageClient extends StorageBucketApi {
100100
void setAuth(String jwt) {
101101
headers['Authorization'] = 'Bearer $jwt';
102102
}
103+
104+
/// Sets an HTTP header for subsequent requests.
105+
///
106+
/// Creates a shallow copy of headers to avoid mutating shared state.
107+
/// Returns this for method chaining.
108+
///
109+
/// ```dart
110+
/// storage.setHeader('x-custom-header', 'value').from('bucket').upload(...);
111+
/// ```
112+
SupabaseStorageClient setHeader(String key, String value) {
113+
headers[key] = value;
114+
return this;
115+
}
103116
}

packages/storage_client/lib/src/storage_file_api.dart

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,34 @@ import 'file_io.dart' if (dart.library.js) './file_stub.dart';
77

88
class StorageFileApi {
99
final String url;
10-
final Map<String, String> headers;
10+
Map<String, String> _headers;
1111
final String? bucketId;
1212
final int _retryAttempts;
1313
final Fetch _storageFetch;
1414

15-
const StorageFileApi(
15+
StorageFileApi(
1616
this.url,
17-
this.headers,
17+
Map<String, String> headers,
1818
this.bucketId,
1919
this._retryAttempts,
2020
this._storageFetch,
21-
);
21+
) : _headers = {...headers};
22+
23+
/// The headers used for requests.
24+
Map<String, String> get headers => _headers;
25+
26+
/// Sets an HTTP header for subsequent requests.
27+
///
28+
/// Creates a shallow copy of headers to avoid mutating shared state.
29+
/// Returns this for method chaining.
30+
///
31+
/// ```dart
32+
/// storage.from('bucket').setHeader('x-custom-header', 'value').upload(...);
33+
/// ```
34+
StorageFileApi setHeader(String key, String value) {
35+
_headers = {..._headers, key: value};
36+
return this;
37+
}
2238

2339
String _getFinalPath(String path) {
2440
return '$bucketId/$path';

packages/storage_client/test/client_test.dart

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import "package:path/path.dart" show join;
55
import 'package:storage_client/storage_client.dart';
66
import 'package:test/test.dart';
77

8+
import 'custom_http_client.dart';
9+
810
const storageUrl = 'http://localhost:8000/storage/v1';
911
const storageKey =
1012
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJhdWQiOiIiLCJzdWIiOiIzMTdlYWRjZS02MzFhLTQ0MjktYTBiYi1mMTlhN2E1MTdiNGEiLCJSb2xlIjoicG9zdGdyZXMifQ.pZobPtp6gDcX0UbzMmG3FHSlg4m4Q-22tKtGWalOrNo';
@@ -479,4 +481,131 @@ void main() {
479481
final res2 = await storage.from(newBucketName).exists('not-exist');
480482
expect(res2, false);
481483
});
484+
485+
group('setHeader', () {
486+
late CustomHttpClient customHttpClient;
487+
late SupabaseStorageClient client;
488+
489+
setUp(() {
490+
customHttpClient = CustomHttpClient();
491+
client = SupabaseStorageClient(
492+
storageUrl,
493+
{'Authorization': 'Bearer $storageKey'},
494+
httpClient: customHttpClient,
495+
);
496+
});
497+
498+
test('sets custom header on storage client', () async {
499+
customHttpClient.response = [];
500+
customHttpClient.statusCode = 200;
501+
502+
client.setHeader('x-custom-header', 'custom-value');
503+
await client.listBuckets();
504+
505+
expect(customHttpClient.receivedRequests.length, 1);
506+
expect(
507+
customHttpClient.receivedRequests.first.headers['x-custom-header'],
508+
'custom-value',
509+
);
510+
});
511+
512+
test('returns this for method chaining', () {
513+
final result = client.setHeader('x-header-a', 'value-a');
514+
expect(identical(result, client), isTrue);
515+
});
516+
517+
test('supports chaining multiple setHeader calls', () async {
518+
customHttpClient.response = [];
519+
customHttpClient.statusCode = 200;
520+
521+
client
522+
.setHeader('x-header-a', 'value-a')
523+
.setHeader('x-header-b', 'value-b');
524+
await client.listBuckets();
525+
526+
expect(customHttpClient.receivedRequests.length, 1);
527+
final headers = customHttpClient.receivedRequests.first.headers;
528+
expect(headers['x-header-a'], 'value-a');
529+
expect(headers['x-header-b'], 'value-b');
530+
});
531+
532+
test('headers set on client are included in file operations', () async {
533+
customHttpClient.response = [];
534+
customHttpClient.statusCode = 200;
535+
536+
client.setHeader('x-custom-header', 'custom-value');
537+
await client.from('test-bucket').list();
538+
539+
expect(customHttpClient.receivedRequests.length, 1);
540+
expect(
541+
customHttpClient.receivedRequests.first.headers['x-custom-header'],
542+
'custom-value',
543+
);
544+
});
545+
546+
test('setHeader on StorageFileApi sets header for that instance', () async {
547+
customHttpClient.response = [];
548+
customHttpClient.statusCode = 200;
549+
550+
final fileApi = client.from('test-bucket');
551+
fileApi.setHeader('x-file-header', 'file-value');
552+
await fileApi.list();
553+
554+
expect(customHttpClient.receivedRequests.length, 1);
555+
expect(
556+
customHttpClient.receivedRequests.first.headers['x-file-header'],
557+
'file-value',
558+
);
559+
});
560+
561+
test(
562+
'setHeader on StorageFileApi does not affect other StorageFileApi instances',
563+
() async {
564+
customHttpClient.response = [];
565+
customHttpClient.statusCode = 200;
566+
567+
final fileApi1 = client.from('bucket1');
568+
final fileApi2 = client.from('bucket2');
569+
570+
fileApi1.setHeader('x-header', 'value1');
571+
572+
await fileApi1.list();
573+
await fileApi2.list();
574+
575+
expect(customHttpClient.receivedRequests.length, 2);
576+
expect(
577+
customHttpClient.receivedRequests[0].headers['x-header'],
578+
'value1',
579+
);
580+
// fileApi2 should not have the header set on fileApi1
581+
expect(
582+
customHttpClient.receivedRequests[1].headers['x-header'],
583+
isNull,
584+
);
585+
});
586+
587+
test('setHeader on StorageFileApi returns this for chaining', () async {
588+
customHttpClient.response = [];
589+
customHttpClient.statusCode = 200;
590+
591+
final fileApi = client.from('test-bucket');
592+
final result = fileApi.setHeader('x-header', 'value');
593+
594+
expect(identical(result, fileApi), isTrue);
595+
});
596+
597+
test('setHeader can override existing headers', () async {
598+
customHttpClient.response = [];
599+
customHttpClient.statusCode = 200;
600+
601+
client.setHeader('Authorization', 'Bearer new-token');
602+
await client.listBuckets();
603+
604+
expect(customHttpClient.receivedRequests.length, 1);
605+
expect(
606+
customHttpClient.receivedRequests.first.headers['Authorization'],
607+
'Bearer new-token',
608+
);
609+
});
610+
});
482611
}

0 commit comments

Comments
 (0)