{"record":{"id":"1ce5cf84254b183c","repo":"TryGhost/Ghost","slug":"failed-to-fetch-image-response-status","errorCode":null,"errorMessage":"Failed to fetch image: ${response.status}","messagePattern":"Failed to fetch image: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"apps/activitypub/src/utils/image.ts","lineNumber":16,"sourceCode":"export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB limit\nexport const FILE_SIZE_ERROR_MESSAGE = 'Image must be less than 5MB in size.';\n\nexport const PROFILE_MAX_DIMENSIONS = {width: 400, height: 400};\nexport const COVER_MAX_DIMENSIONS = {width: 4000, height: 3000};\n\n/**\n * Converts an image URL to a data URL to avoid CORS issues\n */\nexport const imageUrlToDataUrl = async (url: string): Promise<string> => {\n    try {\n        const response = await fetch(url, {\n            mode: 'cors'\n        });\n        if (!response.ok) {\n            throw new Error(`Failed to fetch image: ${response.status}`);\n        }\n        const blob = await response.blob();\n        return new Promise((resolve, reject) => {\n            const reader = new FileReader();\n            reader.onload = () => resolve(reader.result as string);\n            reader.onerror = reject;\n            reader.readAsDataURL(blob);\n        });\n    } catch {\n        // Return original URL as fallback if conversion fails\n        return url;\n    }\n};\n\n/**\n * Checks if an image file's dimensions are within specified maximum limits\n */\nexport const checkImageDimensions = (","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/activitypub/src/utils/image.ts#L1-L34","documentation":"Thrown inside imageUrlToDataUrl() when the image fetch returns a non-2xx status. Crucially, this throw lives inside a try block whose catch (image.ts:25) swallows ALL errors and returns the original URL — so this error NEVER propagates to the caller. It is an internally-suppressed error; the function degrades silently to returning the unconverted URL, which may reintroduce the CORS issue the conversion was meant to avoid.","triggerScenarios":"imageUrlToDataUrl() is called on a URL whose server responds 404 (image removed), 403 (hotlink protection), 5xx, or any non-ok status. A network/CORS TypeError from fetch() hits the same catch and is swallowed identically.","commonSituations":"Remote avatar/cover images from ActivityPub peers whose servers are down or block cross-origin fetches; image URLs that have expired or moved; a peer that returns HTML (login page) instead of an image, producing a non-ok or unparseable response.","solutions":["Treat a returned value equal to the input URL as a signal that conversion failed; verify the image renders downstream rather than assuming you got a data URL.","Pre-validate the URL (same-origin proxy, reachable host) before calling, or proxy the image through your own server to avoid CORS/non-ok responses.","If you actually need to observe the failure, fork the logic and re-throw from the catch instead of silently returning the original URL."],"exampleFix":"// before: failure is invisible — caller cannot tell data-URL conversion failed\nconst src = await imageUrlToDataUrl(url);\n\n// after: detect the silent fallback\nconst src = await imageUrlToDataUrl(url);\nconst isDataUrl = src.startsWith('data:');\nif (!isDataUrl) {\n    // conversion failed (non-ok fetch or CORS) — handle degraded path\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"// The error is swallowed internally and the original URL is returned.\n// Detect the silent fallback by comparing input to output:\nconst src = await imageUrlToDataUrl(url);\nif (!src.startsWith('data:')) {\n    // conversion failed (non-ok fetch or CORS) — handle the degraded path,\n    // e.g. render the <img> directly and accept possible CORS-tainted canvas.\n}","preventionTips":["Treat a returned value equal to (or not a data: URL of) the input as 'conversion failed'.","Proxy remote/peer images through your own origin to avoid CORS and non-ok responses.","If you need to observe the real failure, wrap the call and re-throw from a custom copy of the function — the stock catch hides the cause."],"tags":["image","network","cors","suppressed-error","fallback","activitypub"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}