-
-
Notifications
You must be signed in to change notification settings - Fork 271
Description
Bug report
- I confirm this is a bug with Supabase, not with my own application.
- I confirm I have searched the Docs, GitHub Discussions, and Discord.
Describe the bug
The uploadToSignedUrl method in the storage client accepts fileOptions (which includes the metadata property), but the implementation completely ignores it. As a result, files uploaded via signed URLs do not have their custom metadata persisted.
This is inconsistent with the standard .upload() method, which correctly processes and attaches metadata to the request body/headers.
To Reproduce
Steps to reproduce the behavior:
- Create a private bucket named
test-bucket. - Generate a signed upload URL for a specific path.
- Use
uploadToSignedUrlto upload a file with custom metadata. - Inspect the file metadata (via the dashboard, SQL, or a database trigger).
Code Snippet:
const path = 'folder/image.jpg';
const { data: { token } } = await supabase.storage
.from('test-bucket')
.createSignedUploadUrl(path);
// Upload with custom metadata
const { data, error } = await supabase.storage
.from('test-bucket')
.uploadToSignedUrl(path, token, myFile, {
metadata: {
custom_id: '12345',
processed: true
}
});
// Result: The file is uploaded, but 'custom_id' and 'processed' are missing from the storage.objects metadata.I verified this using a Postgres function triggered on INSERT to storage.objects. The system metadata (size, mimetype) is present, but the custom metadata passed in fileOptions is completely missing.
Postgres Trigger Code:
DECLARE
v_custom_field text;
BEGIN
-- Log the full metadata object
RAISE LOG 'Metadata: %', jsonb_pretty(NEW.metadata);
-- Attempt to grab the custom key
v_custom_field := NEW.metadata ->> 'custom_field';
-- v_custom_field is NULL here
...
Resulting Log Output:
The log shows that NEW.metadata only contains system fields. The custom_field is nowhere to be found.
"Metadata: {
"eTag": "\"ce344cb1535357b3567fa12bf196287d\"",
"size": 91955,
"mimetype": "image/jpeg",
"cacheControl": "max-age=undefined",
"lastModified": "2026-01-10T04:12:42.000Z",
"contentLength": 91955,
"httpStatusCode": 200
}"
Expected behavior
The uploadToSignedUrl method should behave like .upload(): it should serialize the options.metadata object and append it to the FormData (or headers) so that Supabase Storage persists it.
Code Analysis (Root Cause)
I reviewed the source code for StorageFileApi.ts.
In the working upload() method (specifically the uploadOrUpdate helper), the code explicitly checks for and appends metadata:
// valid implementation in uploadOrUpdate
if (metadata) {
body.append('metadata', this.encodeMetadata(metadata))
}However, in uploadToSignedUrl, this logic is missing entirely. The options object is created but the metadata property is never accessed or used.
Current implementation of uploadToSignedUrl:
async uploadToSignedUrl(...) {
// ...
const options = { upsert: DEFAULT_FILE_OPTIONS.upsert, ...fileOptions }
if (typeof Blob !== 'undefined' && fileBody instanceof Blob) {
body = new FormData()
body.append('cacheControl', options.cacheControl as string)
// ❌ MISSING: No check for options.metadata here
body.append('', fileBody)
}
// ...
}System information
- Windows 11
- Chrome 143
- supabase-js: 2.90.1
- Node: 22.18.0
Additional context
The fix is likely to copy the metadata handling logic from uploadOrUpdate into uploadToSignedUrl. Specifically, appending the metadata field to the FormData when it exists.