Skip to content

uploadToSignedUrl ignores metadata in fileOptions (inconsistent with .upload()) #823

@Velkamhell97

Description

@Velkamhell97

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:

  1. Create a private bucket named test-bucket.
  2. Generate a signed upload URL for a specific path.
  3. Use uploadToSignedUrl to upload a file with custom metadata.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions