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

  1. 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.
  2. 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.
  3. 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

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


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/1ce5cf84254b183c. Report an issue: GitHub.