name: supabase-storage-400-debug
description: Debug Supabase Storage 400 errors when supabase.storage.from().download() fails for all files
Supabase Storage 400 Debug Checklist
Symptoms
supabase.storage.from("bucket").download() returns 400 for all files. Console shows:
`
https://xxx.supabase.co/storage/v1/object/bucket-name/path/to/file 400 (Bad Request)
`
Diagnostic Steps
1. Check if bucket exists — Query storage REST API directly:
`bash
curl -s 'https://PROJECT.supabase.co/storage/v1/bucket' \
-H 'Authorization: Bearer ANON_KEY'
`
- [] = bucket does NOT exist
- Returns objects = bucket exists (check RLS/permissions)
2. Check RLS on storage.objects — Even if bucket exists, RLS may block access:
`sql
SELECT * FROM storage.objects LIMIT 1;
`
(If table doesn't exist or query fails, storage may not be configured)
3. Check actual file storage path — The app may store files locally on VPS instead of Supabase Storage. Look at the upload code:
- If upload writes to /var/www/... → files are on VPS filesystem, not Supabase Storage
- Database storage_path column tells WHERE the file is
4. Check nginx alias — Verify nginx config serves the right directory:
`
location /uploads/ {
alias /var/www/documentos/uploads/; # must match actual storage path
expires 30d;
}
`
5. Test public URL directly — curl -I https://domain/uploads/... returns 200? If yes, switch to direct URL fetch instead of Supabase Storage API.
Fix Pattern: Switch to Direct URL Fetch
When Supabase Storage bucket doesn't exist but files are on VPS:
`typescript
const PUBLIC_URL = "https://your-domain.com";
function storageUrl(storagePath: string) {
return ${PUBLIC_URL}${storagePath};
}
async function openPreview(doc: Doc) {
try {
const res = await fetch(storageUrl(doc.storage_path));
if (!res.ok) throw new Error(HTTP ${res.status});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setPreview({ doc, url });
} catch {
toast.error("Falha ao abrir documento");
}
}
async function downloadZip(docs: Doc[]) {
const zip = new JSZip();
for (const d of docs) {
try {
const res = await fetch(storageUrl(d.storage_path));
if (res.ok) zip.file(d.stored_name, await res.blob());
} catch { / skip failed files / }
}
triggerBlob(await zip.generateAsync({ type: "blob" }), "arquivos.zip");
}
`
Security note: Direct URL files are public. If files need authentication, create a proxy endpoint that validates the session before serving.
Root Cause Pattern
Lovable-generated apps sometimes store uploads on VPS filesystem (/var/www/...) while the Supabase Storage bucket was never created. The storage_path in the DB is a filesystem path like /uploads/userId/clientId/storedName, not a Supabase Storage path.