Image Upload Failing
This page covers common failures in the editor's image upload flow.
File exceeds size limit
What you see: The upload callback fires with progress: 0 or your server returns a 413 response. The editor may show "Upload failed".
Why it happens: The selected file is larger than your server's or proxy's maximum upload size.
Fix:
- Check and increase your server-side limit:
// Express
app.use(express.json({ limit: '10mb' }));
// Nginx (in nginx.conf or site config)
// client_max_body_size 10m;
- Optionally, resize on the client before uploading:
onImageUpload={(file, done) => {
if (file.size > 5 * 1024 * 1024) {
alert('Please select an image under 5 MB');
done({ progress: 0 });
return;
}
uploadFile(file).then((url) => done({ progress: 100, url }));
}}
CORS error with external storage
What you see: Browser console shows Access to XMLHttpRequest at 'https://storage.example.com/...' has been blocked by CORS policy.
Why it happens: Your storage service (S3, GCS, Azure Blob) doesn't allow requests from your app's origin.
Fix: Configure CORS on your storage bucket.
AWS S3:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT", "POST"],
"AllowedOrigins": ["https://your-app.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
Google Cloud Storage:
[
{
"origin": ["https://your-app.com"],
"method": ["PUT", "POST"],
"responseHeader": ["Content-Type"],
"maxAgeSeconds": 3600
}
]
If you upload via your own API (recommended), CORS only needs to be set on your API server, not the storage bucket.
Presigned URL has expired
What you see: The upload starts but the storage service returns a 403 Forbidden, and the console shows Request has expired or Signature expired.
Why it happens: Presigned upload URLs have a TTL. If the user waits too long between generating the URL and completing the upload, the signature expires.
Fix: Generate a fresh presigned URL inside the onImageUpload callback so it's created at upload time, not at page load.
onImageUpload={async (file, done) => {
// Get a fresh presigned URL right now
const res = await fetch('/api/upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
const { uploadUrl, publicUrl } = await res.json();
// Upload directly to storage
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
done({ progress: 100, url: publicUrl });
}}
Set a reasonable TTL (5-15 minutes) on the presigned URL to account for slow connections.
Unsupported image format
What you see: The upload succeeds but the image doesn't render in the editor, or onImageUpload is never triggered for certain files.
Why it happens: The editor's file picker filters by supported formats. Formats like .webp, .svg, .heic, or .bmp may not be accepted by default, and some email clients don't support them.
Fix:
- Convert unsupported formats server-side before returning the URL:
// Server-side (Node.js with sharp)
const sharp = require('sharp');
app.post('/api/images/upload', upload.single('file'), async (req, res) => {
const buffer = await sharp(req.file.buffer)
.toFormat('png')
.toBuffer();
const url = await saveToStorage(buffer, 'image/png');
res.json({ url });
});
- Validate the format in the upload callback:
const SUPPORTED = ['image/jpeg', 'image/png', 'image/gif'];
onImageUpload={(file, done) => {
if (!SUPPORTED.includes(file.type)) {
alert(`Unsupported format: ${file.type}. Use JPEG, PNG, or GIF.`);
done({ progress: 0 });
return;
}
uploadFile(file).then((url) => done({ progress: 100, url }));
}}
Quick checklist
| Check | How to verify |
|---|---|
| File size within limit | Log file.size in onImageUpload |
| CORS configured on storage | Browser console has no CORS errors |
| Presigned URL not expired | Generate URL inside the upload callback |
| Format is JPEG, PNG, or GIF | Log file.type and verify |
done() is always called | Every code path (success and error) must call done() |