{"record":{"id":"3b23111883cee5ce","repo":"odysseus-dev/odysseus","slug":"empty-image","errorCode":null,"errorMessage":"Empty image","messagePattern":"Empty image","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"static/js/emailLibrary.js","lineNumber":5994,"sourceCode":"      showError();\n    }, { once: true });\n    if (isCidImage || isRemoteImage) {\n      loadDirect();\n      setTimeout(() => {\n        if (!settled && frame) {\n          frame.classList.add('is-loading');\n        }\n      }, 12000);\n      return;\n    }\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), 20000);\n    fetch(loadSrc, { credentials: 'same-origin', signal: controller.signal })\n      .then(async res => {\n        clearTimeout(timeoutId);\n        if (!res.ok) throw new Error(`HTTP ${res.status}`);\n        const blob = await res.blob();\n        if (!blob || !blob.size) throw new Error('Empty image');\n        const type = blob.type && blob.type.startsWith('image/') ? blob.type : 'image/png';\n        objectUrl = URL.createObjectURL(blob.type === type ? blob : new Blob([blob], { type }));\n        ensureFrame();\n        img.src = objectUrl;\n        if (img.complete && img.naturalWidth > 0) showLoaded();\n        if (!settled && typeof img.decode === 'function') {\n          img.decode().then(showLoaded).catch(() => {\n            if (img.complete && img.naturalWidth === 0) showError();\n          });\n        }\n      })\n      .catch(err => {\n        clearTimeout(timeoutId);\n        showError(err?.name === 'AbortError' ? new Error('request timed out') : err);\n      });\n    setTimeout(() => {\n      if (!settled && !frame) {\n        ph.classList.remove('is-loading');","sourceCodeStart":5976,"sourceCodeEnd":6012,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/emailLibrary.js#L5976-L6012","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const blob = await res.blob();\nif (!blob || !blob.size || blob.size < 100) throw new Error('Empty image');","typeGuard":"function isUsableImageBlob(b) { return b != null && typeof b.size === 'number' && b.size > 0 && /image\\/|application\\/octet-stream/.test(b.type || ''); }","tryCatchPattern":".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(); });","preventionTips":["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"],"tags":["email","image-proxy","empty-response","fetch","blob"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}