TryGhost/Ghost · info · Error
Failed to fetch image: ${response.status}
Error message
Failed to fetch image: ${response.status} What it means
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.
Source
Thrown at apps/activitypub/src/utils/image.ts:16
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB limit
export const FILE_SIZE_ERROR_MESSAGE = 'Image must be less than 5MB in size.';
export const PROFILE_MAX_DIMENSIONS = {width: 400, height: 400};
export const COVER_MAX_DIMENSIONS = {width: 4000, height: 3000};
/**
* Converts an image URL to a data URL to avoid CORS issues
*/
export const imageUrlToDataUrl = async (url: string): Promise<string> => {
try {
const response = await fetch(url, {
mode: 'cors'
});
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status}`);
}
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch {
// Return original URL as fallback if conversion fails
return url;
}
};
/**
* Checks if an image file's dimensions are within specified maximum limits
*/
export const checkImageDimensions = (View on GitHub (pinned to 47d8b0e2ad)
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.
Example fix
// before: failure is invisible — caller cannot tell data-URL conversion failed
const src = await imageUrlToDataUrl(url);
// after: detect the silent fallback
const src = await imageUrlToDataUrl(url);
const isDataUrl = src.startsWith('data:');
if (!isDataUrl) {
// conversion failed (non-ok fetch or CORS) — handle degraded path
} Defensive patterns
Strategy: fallback
Try / catch
// The error is swallowed internally and the original URL is returned.
// Detect the silent fallback by comparing input to output:
const src = await imageUrlToDataUrl(url);
if (!src.startsWith('data:')) {
// conversion failed (non-ok fetch or CORS) — handle the degraded path,
// e.g. render the <img> directly and accept possible CORS-tainted canvas.
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Clipboard API not supported in this browser
- Request timed out, please try again.
- Download failed: ${response.status} ${response.statusText}
- Failed to fetch changelog: ${response.status}
- Failed to fetch site data
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/1ce5cf84254b183c.
Report an issue: GitHub.