Skip to content

Commit 99d9136

Browse files
grdsdevclaudeVinzent03
authored
fix(storage): avoid duplicate Content-Type headers and header mutation (#1359)
* fix(storage): avoid duplicate Content-Type headers and header mutation In _handleRequest, the headers map was being mutated directly by adding Content-Type: application/json. This meant: 1. The stored headers map on StorageFileApi/_StorageBucketApi was permanently mutated after each non-GET request, accumulating Content-Type. 2. Custom Content-Type headers set via setHeader() were overwritten unconditionally, ignoring caller intent. Fix: copy headers before modifying, and use a case-insensitive check for an existing Content-Type before setting the application/json default. Linear: SDK-881 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style(storage): format fetch.dart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update packages/storage_client/lib/src/fetch.dart Co-authored-by: Vinzent <vinzent03@proton.me> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Vinzent <vinzent03@proton.me>
1 parent 9d0de9e commit 99d9136

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

packages/storage_client/lib/src/fetch.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,13 @@ class Fetch {
6969
Map<String, dynamic>? body,
7070
FetchOptions? options,
7171
) async {
72-
final headers = options?.headers ?? {};
72+
final headers = {...?options?.headers};
7373
if (method != 'GET') {
74-
headers['Content-Type'] = 'application/json';
74+
final hasContentType =
75+
headers.keys.any((key) => key.toLowerCase() == 'content-type');
76+
if (!hasContentType) {
77+
headers['Content-Type'] = 'application/json';
78+
}
7579
}
7680

7781
final request = http.Request(method, Uri.parse(url))

packages/storage_client/test/client_test.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,4 +608,58 @@ void main() {
608608
);
609609
});
610610
});
611+
612+
group('Content-Type header handling', () {
613+
late CustomHttpClient customHttpClient;
614+
late SupabaseStorageClient client;
615+
616+
setUp(() {
617+
customHttpClient = CustomHttpClient();
618+
client = SupabaseStorageClient(
619+
storageUrl,
620+
{'Authorization': 'Bearer $storageKey'},
621+
httpClient: customHttpClient,
622+
);
623+
});
624+
625+
test('defaults to application/json for non-GET requests', () async {
626+
customHttpClient.response = {'message': 'Emptied'};
627+
customHttpClient.statusCode = 200;
628+
629+
await client.emptyBucket('bucket1');
630+
631+
expect(customHttpClient.receivedRequests.length, 1);
632+
expect(
633+
customHttpClient.receivedRequests.first.headers['content-type'],
634+
contains('application/json'),
635+
);
636+
});
637+
638+
test('preserves custom Content-Type set via setHeader', () async {
639+
customHttpClient.response = {'message': 'Emptied'};
640+
customHttpClient.statusCode = 200;
641+
642+
client.setHeader('Content-Type', 'application/octet-stream');
643+
await client.emptyBucket('bucket1');
644+
645+
expect(customHttpClient.receivedRequests.length, 1);
646+
expect(
647+
customHttpClient.receivedRequests.first.headers['content-type'],
648+
startsWith('application/octet-stream'),
649+
);
650+
});
651+
652+
test('does not mutate the stored headers map after a non-GET request',
653+
() async {
654+
customHttpClient.response = [];
655+
customHttpClient.statusCode = 200;
656+
657+
final fileApi = client.from('test-bucket');
658+
final headersBefore = Map<String, String>.from(fileApi.headers);
659+
660+
await fileApi.list();
661+
662+
expect(fileApi.headers, equals(headersBefore));
663+
});
664+
});
611665
}

0 commit comments

Comments
 (0)