This error occurs when attempting to upload a file to Supabase Storage at a path where a file already exists. By default, Supabase rejects file uploads that would overwrite existing resources. The solution is to enable the upsert option to allow file replacement or delete the existing file first.
The "Resource already exists" error (HTTP 409 Conflict) is thrown by Supabase Storage API when you attempt to upload a file to a location where a file already exists. Unlike some systems that overwrite files by default, Supabase's storage layer is designed to prevent accidental overwrites, making this an intentional safety mechanism. This error typically occurs during file upload operations and indicates that the specified file path is already occupied. It's distinct from bucket-level errors—the bucket exists and is accessible, but the specific file path is taken.
The simplest solution is to set upsert: true in your upload options. This tells Supabase to overwrite the file if it already exists:
const { data, error } = await supabase.storage
.from('bucket-name')
.upload('path/to/file.jpg', file, {
upsert: true // Allows overwriting existing files
});
if (error) {
console.error('Upload error:', error.message);
} else {
console.log('File uploaded successfully:', data);
}The upsert option is available in the FileOptions object for the upload() method. When set to true, if a file exists at the specified path, it will be replaced with the new file.
Alternatively, you can check whether a file already exists and handle it explicitly:
const filePath = 'path/to/file.jpg';
// List files to check if it exists
const { data: existingFiles, error: listError } = await supabase.storage
.from('bucket-name')
.list('path/to');
if (listError) {
console.error('List error:', listError.message);
return;
}
const fileExists = existingFiles.some(f => f.name === 'file.jpg');
if (fileExists) {
// Delete the old file first
const { error: deleteError } = await supabase.storage
.from('bucket-name')
.remove([filePath]);
if (deleteError) {
console.error('Delete error:', deleteError.message);
return;
}
}
// Now upload the new file
const { data, error } = await supabase.storage
.from('bucket-name')
.upload(filePath, file);
if (error) {
console.error('Upload error:', error.message);
} else {
console.log('File uploaded:', data);
}This approach gives you explicit control and lets you log the deletion of old files.
For cases where you want to keep multiple versions or avoid overwrites entirely, generate unique filenames:
import { v4 as uuidv4 } from 'uuid';
const originalFileName = file.name;
const timestamp = new Date().getTime();
const fileExtension = originalFileName.split('.').pop();
// Option 1: Timestamp-based
const uniquePath = `uploads/${timestamp}-${originalFileName}`;
// Option 2: UUID-based (recommended)
const uniqueId = uuidv4();
const uniquePath = `uploads/${uniqueId}.${fileExtension}`;
const { data, error } = await supabase.storage
.from('bucket-name')
.upload(uniquePath, file); // No upsert needed, path is always unique
if (error) {
console.error('Upload error:', error.message);
} else {
console.log('File uploaded:', data);
return uniquePath; // Return the unique path for storage
}Store the returned unique path in your database so you can reference and download the file later.
When using upsert: true, ensure your Row Level Security policies allow UPDATE operations on storage.objects:
-- In Supabase SQL Editor, add an UPDATE policy
create policy "Allow users to update their own files"
on storage.objects
for update
to authenticated
using ( auth.uid()::text = owner_id )
with check ( auth.uid()::text = owner_id );
-- Or for development (allow all - be careful in production)
create policy "Allow upsert for all users"
on storage.objects
for update
using ( true )
with check ( true );Without an UPDATE policy, upsert operations will fail with a 403 Forbidden error. Ensure you have both INSERT and UPDATE policies for full upsert functionality.
Add detailed logging to understand whether you're hitting the resource-exists error or a different issue:
async function uploadWithDebug(bucketName, filePath, file) {
console.log(`Attempting upload to: ${bucketName}/${filePath}`);
const { data, error } = await supabase.storage
.from(bucketName)
.upload(filePath, file, {
upsert: false // Set to false to see if file exists
});
if (error) {
console.error('Upload failed:', {
message: error.message,
status: error.status,
statusCode: error.statusCode,
cause: error.cause
});
// Check if it's specifically the "resource already exists" error
if (error.message.includes('resource_already_exists') || error.status === 409) {
console.log('File already exists at this path. Retry with upsert: true');
// Retry with upsert
const { data: retryData, error: retryError } = await supabase.storage
.from(bucketName)
.upload(filePath, file, {
upsert: true
});
if (retryError) {
console.error('Upsert also failed:', retryError);
} else {
console.log('File upserted successfully:', retryData);
return retryData;
}
}
} else {
console.log('File uploaded successfully:', data);
return data;
}
}This logging helps identify whether the issue is truly a conflict or a different permission/policy problem.
Upsert vs Update: The upsert: true option performs an INSERT or UPDATE operation automatically. If the file doesn't exist, it's inserted. If it exists, it's updated/replaced. This is more efficient than checking existence separately.
File Metadata Loss: When you overwrite a file using upsert, the new file does not inherit metadata from the previous version. If you've set custom metadata (cache-control, content-type, etc.) on the original file, you need to specify it again in the upsert call.
Race Conditions: In high-concurrency scenarios where multiple uploads to the same path occur simultaneously, using upsert with random filenames is safer than relying on sequential checks. Consider distributing writes across different paths when possible.
Storage Objects vs Signed URLs: Creating a signed URL with createSignedUploadUrl on an existing file path will also fail with the same error. If you're using signed URLs for uploads, you must either enable upsert or generate unique paths.
Cache Control and Content-Type: When upserting files, remember to explicitly set cache-control headers if needed:
await supabase.storage
.from('bucket')
.upload(path, file, {
upsert: true,
contentType: 'image/jpeg',
cacheControl: '3600' // 1 hour
});Without explicit content-type, Supabase may incorrectly detect file types, especially for binary files.
Concurrent Uploads to Same Path: If multiple processes try to upload to the same path simultaneously, race conditions may occur even with upsert enabled. For atomic file operations, consider using unique temporary names and renaming afterward, or using transactions at the application level.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
email_address_not_authorized: Email sending to this address is not authorized
Email address not authorized for sending in Supabase Auth