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

  1. Retry the load (re-open the message) — truncated IMAP fetches are often transient
  2. Confirm via curl that the proxy URL returns non-zero bytes with a fresh session
  3. If you own the proxy, fail with 502 instead of 200-on-empty so this branch is rare
  4. 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

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.