odysseus-dev/odysseus · warning · Error
Empty image
Error message
Empty image
What it means
Thrown when the proxied image request returned 2xx but the response blob is empty (size 0). Treated identically to a failed load: the frame shows the error placeholder. A 200-with-zero-bytes usually indicates a server/proxy artifact rather than a missing asset (which would be non-2xx).
Source
Thrown at static/js/emailLibrary.js:5994
showError();
}, { once: true });
if (isCidImage || isRemoteImage) {
loadDirect();
setTimeout(() => {
if (!settled && frame) {
frame.classList.add('is-loading');
}
}, 12000);
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 20000);
fetch(loadSrc, { credentials: 'same-origin', signal: controller.signal })
.then(async res => {
clearTimeout(timeoutId);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob();
if (!blob || !blob.size) throw new Error('Empty image');
const type = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/png';
objectUrl = URL.createObjectURL(blob.type === type ? blob : new Blob([blob], { type }));
ensureFrame();
img.src = objectUrl;
if (img.complete && img.naturalWidth > 0) showLoaded();
if (!settled && typeof img.decode === 'function') {
img.decode().then(showLoaded).catch(() => {
if (img.complete && img.naturalWidth === 0) showError();
});
}
})
.catch(err => {
clearTimeout(timeoutId);
showError(err?.name === 'AbortError' ? new Error('request timed out') : err);
});
setTimeout(() => {
if (!settled && !frame) {
ph.classList.remove('is-loading');View on GitHub (pinned to f9235ebbf1)
Solutions
- Retry the load (re-open the message) — truncated IMAP fetches are often transient
- Confirm via curl that the proxy URL returns non-zero bytes with a fresh session
- If you own the proxy, fail with 502 instead of 200-on-empty so this branch is rare
- Check server logs for the corresponding attachment fetch to find where the bytes were lost
Defensive patterns
Strategy: validation
Validate before calling
const blob = await res.blob();
if (!blob || !blob.size || blob.size < 100) throw new Error('Empty image'); Type guard
function isUsableImageBlob(b) { return b != null && typeof b.size === 'number' && b.size > 0 && /image\/|application\/octet-stream/.test(b.type || ''); } Try / catch
.then(async res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); if (!isUsableImageBlob(blob)) throw new Error('Empty image'); ... }).catch(err => { if (!settled) showError(); }); Prevention
- Validate blob.size (and optionally a minimum byte floor) before createObjectURL
- Revoke object URLs on error to avoid leaks
- Retry once on empty 200s — truncated IMAP part fetches are often transient
- Server-side: return 502 on empty upstream bodies instead of 200-with-zero-bytes
When it happens
Trigger: The attachment proxy streams the IMAP body part but an upstream fetch inside it returns an empty body; a CDN edge serving a cached empty 200; a truncated response after a dropped connection that still finalized with 200.
Common situations: Attachment part partially fetched when the IMAP session dropped; misconfigured proxy returning Response(null) with 200; hotlink-protected remotes answering 200 with empty payloads to discourage scraping.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/3b23111883cee5ce.
Report an issue: GitHub.