Problem
When serving images through Cloudflare Images in a Worker, fetching via the CDN transformation URL sometimes fails even though the image exists. This causes the Worker to regenerate expensive assets (like headless screenshots) on every request instead of serving the cached version:
async function getCachedImage(key: string, env: Env): Promise<Response | null> {
const response = await fetch(
`${env.IMAGE_URL}/${key}/quality=80,width=500,format=auto,fit=cover`
)
if (response.ok) return response
// Image exists but CDN transform failed -- falls through to expensive regeneration
return null
}
The CDN transformation endpoint (imagedelivery.net/<hash>/<id>/<transforms>) can return errors for valid images due to propagation delays, transformation limits, or transient CDN issues. Without a fallback, every failure triggers a full re-capture and re-upload cycle.
Solution
Add a second fetch tier that retrieves the raw image blob via the Cloudflare Images API when the CDN transformation URL fails:
async function getCachedImage(
key: string,
env: Env,
headers: Headers,
params: URLSearchParams,
): Promise<Response | null> {
// Tier 1: Try CDN transformation URL (fast, edge-cached, supports on-demand transforms)
try {
const transforms = [
`quality=${params.get('quality') || '80'}`,
`width=${params.get('width') || '500'}`,
`format=${params.get('format') || 'auto'}`,
`fit=${params.get('fit') || 'cover'}`,
]
for (const name of ['height', 'gravity'] as const) {
const value = params.get(name)
if (value) transforms.push(`${name}=${value}`)
}
const response = await fetch(
`${env.IMAGE_URL}/${key}/${transforms.join(',')}`,
{ headers },
)
if (response.ok || response.status === 304) return response
if (response.status !== 404 && response.status !== 204) {
console.error('Image resize error:', response.status)
}
} catch (error) {
console.error('Image resize error:', error)
}
// Tier 2: Fallback to raw blob via Cloudflare Images API (slower, but confirms image exists)
try {
const response = await fetch(`${env.CLOUDFLARE_API_URL}/${key}/blob`, {
headers: { Authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}` },
})
if (response.ok && response.body) {
return new Response(response.body, {
status: 200,
headers: {
'Content-Type': 'image/jpeg',
'Cache-Control': 'public, max-age=86400',
},
})
}
if (response.status !== 404 && response.status !== 204) {
console.error('Image fetch error:', response.status)
}
} catch (error) {
console.error('Image fetch error:', error)
}
return null
}
Why It Works
Cloudflare Images exposes two distinct access paths. The CDN transformation URL (imagedelivery.net/<hash>/<id>/<transforms>) serves images through Cloudflare's edge network with on-demand resizing, format conversion, and caching. The Images API (api.cloudflare.com/client/v4/accounts/<id>/images/v1/<id>/blob) fetches the original uploaded blob directly from storage.
The CDN path is faster and cheaper for repeat requests because it leverages edge caching and avoids API rate limits. However, it can fail transiently -- during propagation after upload, when transformation parameters hit limits, or during edge cache purges. The API blob endpoint bypasses the CDN entirely and reads from origin storage, so it succeeds as long as the image was uploaded. By trying the CDN first and falling back to the API, the Worker avoids regenerating expensive assets while still delivering images with minimal latency on the happy path.
Context
- Cloudflare Images API v1 required; the
/blobendpoint is available on all plans - CDN transformation URL format:
https://imagedelivery.net/<account-hash>/<image-id>/<transforms> - API blob URL format:
https://api.cloudflare.com/client/v4/accounts/<account-id>/images/v1/<image-id>/blob - The API blob fallback does not apply CDN transforms (quality, width, format) -- it returns the original upload
- Silently skip 404/204 status codes in error logging since they indicate the image genuinely does not exist
- The API endpoint requires a bearer token with
Images:Editpermission