Skip to main content

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:

  1. 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;
  1. 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:

  1. 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 });
});
  1. 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

CheckHow to verify
File size within limitLog file.size in onImageUpload
CORS configured on storageBrowser console has no CORS errors
Presigned URL not expiredGenerate URL inside the upload callback
Format is JPEG, PNG, or GIFLog file.type and verify
done() is always calledEvery code path (success and error) must call done()