564 lines
18 KiB
Markdown
564 lines
18 KiB
Markdown
---
|
|
name: cloudflare-images
|
|
description: |
|
|
Store and transform images with Cloudflare Images API and transformations. Use when: uploading images, implementing direct creator uploads, creating variants, generating signed URLs, optimizing formats (WebP/AVIF), transforming via Workers, or debugging CORS, multipart, or error codes 9401-9413.
|
|
license: MIT
|
|
---
|
|
|
|
# Cloudflare Images
|
|
|
|
**Status**: Production Ready ✅
|
|
**Last Updated**: 2025-11-23
|
|
**Dependencies**: Cloudflare account with Images enabled
|
|
**Latest Versions**: Cloudflare Images API v2, @cloudflare/workers-types@4.20251121.0
|
|
|
|
**Recent Updates (2025)**:
|
|
- **August 2025**: AI Face Cropping GA (`gravity=face` with `zoom` control, GPU-based RetinaFace, 99.4% precision)
|
|
- **May 2025**: Media Transformations origin restrictions (default: same-domain only, configurable via dashboard)
|
|
- **Upcoming**: Background removal, generative upscale (planned features)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Two features: **Images API** (upload/store with variants) and **Image Transformations** (resize any image via URL or Workers).
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
**1. Enable**: Dashboard → Images → Get Account ID + API token (Cloudflare Images: Edit permission)
|
|
|
|
**2. Upload**:
|
|
```bash
|
|
curl -X POST https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/images/v1 \
|
|
-H 'Authorization: Bearer <API_TOKEN>' \
|
|
-H 'Content-Type: multipart/form-data' \
|
|
-F 'file=@./image.jpg'
|
|
```
|
|
|
|
**3. Serve**: `https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/public`
|
|
|
|
**4. Transform** (optional): Dashboard → Images → Transformations → Enable for zone
|
|
```html
|
|
<img src="/cdn-cgi/image/width=800,quality=85/uploads/photo.jpg" />
|
|
```
|
|
|
|
---
|
|
|
|
## Upload Methods
|
|
|
|
**1. File Upload**: POST to `/images/v1` with `file` (multipart/form-data), optional `id`, `requireSignedURLs`, `metadata`
|
|
|
|
**2. Upload via URL**: POST with `url=https://example.com/image.jpg` (supports HTTP basic auth)
|
|
|
|
**3. Direct Creator Upload** (one-time URLs, no API key exposure):
|
|
|
|
Backend: POST to `/images/v2/direct_upload` → returns `uploadURL`
|
|
Frontend: POST file to `uploadURL` with FormData
|
|
|
|
**CRITICAL CORS FIX**:
|
|
- ✅ Use `multipart/form-data` (let browser set header)
|
|
- ✅ Name field `file` (NOT `image`)
|
|
- ✅ Call `/direct_upload` from backend only
|
|
- ❌ Don't set `Content-Type: application/json`
|
|
- ❌ Don't call `/direct_upload` from browser
|
|
|
|
---
|
|
|
|
## Image Transformations
|
|
|
|
**URL**: `/cdn-cgi/image/<OPTIONS>/<SOURCE>`
|
|
- Sizing: `width=800,height=600,fit=cover`
|
|
- Quality: `quality=85` (1-100)
|
|
- Format: `format=auto` (WebP/AVIF auto-detection)
|
|
- **Cropping**: `gravity=auto` (smart crop), **`gravity=face`** (AI face detection, Aug 2025 GA), `gravity=center`, **`zoom=0.5`** (0-1 range, face crop tightness)
|
|
- Effects: `blur=10,sharpen=3,brightness=1.2`
|
|
- Fit: `scale-down`, `contain`, `cover`, `crop`, `pad`
|
|
|
|
**Workers**: Use `cf.image` object in fetch
|
|
```typescript
|
|
fetch(imageURL, {
|
|
cf: {
|
|
image: { width: 800, quality: 85, format: 'auto', gravity: 'face', zoom: 0.8 }
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Variants
|
|
|
|
**Named Variants** (up to 100): Predefined transformations (e.g., `avatar`, `thumbnail`)
|
|
- Create: POST to `/images/v1/variants` with `id`, `options`
|
|
- Use: `imagedelivery.net/<HASH>/<ID>/avatar`
|
|
- Works with signed URLs
|
|
|
|
**Flexible Variants**: Dynamic params in URL (`w=400,sharpen=3`)
|
|
- Enable: PATCH `/images/v1/config` with `{"flexible_variants": true}`
|
|
- ❌ **Cannot use with signed URLs** (use named variants instead)
|
|
|
|
---
|
|
|
|
## Signed URLs
|
|
|
|
Generate HMAC-SHA256 tokens for private images (URL format: `?exp=<TIMESTAMP>&sig=<HMAC>`).
|
|
|
|
**Algorithm**: `HMAC-SHA256(signingKey, imageId + variant + expiry)` → hex signature
|
|
|
|
**See**: `templates/signed-urls-generation.ts` for Workers implementation
|
|
|
|
---
|
|
|
|
## Critical Rules
|
|
|
|
### Always Do
|
|
|
|
✅ Use `multipart/form-data` for Direct Creator Upload
|
|
✅ Name the file field `file` (not `image` or other names)
|
|
✅ Call `/direct_upload` API from backend only (NOT browser)
|
|
✅ Use HTTPS URLs for transformations (HTTP not supported)
|
|
✅ URL-encode special characters in image paths
|
|
✅ Enable transformations on zone before using `/cdn-cgi/image/`
|
|
✅ Use named variants for private images (signed URLs)
|
|
✅ Check `Cf-Resized` header for transformation errors
|
|
✅ Set `format=auto` for automatic WebP/AVIF conversion
|
|
✅ Use `fit=scale-down` to prevent unwanted enlargement
|
|
|
|
### Never Do
|
|
|
|
❌ Use `application/json` Content-Type for file uploads
|
|
❌ Call `/direct_upload` from browser (CORS will fail)
|
|
❌ Use flexible variants with `requireSignedURLs=true`
|
|
❌ Resize SVG files (they're inherently scalable)
|
|
❌ Use HTTP URLs for transformations (HTTPS only)
|
|
❌ Put spaces or unescaped Unicode in URLs
|
|
❌ Transform the same image multiple times in Workers (causes 9403 loop)
|
|
❌ Exceed 100 megapixels image size
|
|
❌ Use `/cdn-cgi/image/` endpoint in Workers (use `cf.image` instead)
|
|
❌ Forget to enable transformations on zone before use
|
|
|
|
---
|
|
|
|
## Known Issues Prevention
|
|
|
|
This skill prevents **13+** documented issues.
|
|
|
|
### Issue #1: Direct Creator Upload CORS Error
|
|
|
|
**Error**: `Access to XMLHttpRequest blocked by CORS policy: Request header field content-type is not allowed`
|
|
|
|
**Source**: [Cloudflare Community #345739](https://community.cloudflare.com/t/direct-image-upload-cors-error/345739), [#368114](https://community.cloudflare.com/t/cloudflare-images-direct-upload-cors-problem/368114)
|
|
|
|
**Why It Happens**: Server CORS settings only allow `multipart/form-data` for Content-Type header
|
|
|
|
**Prevention**:
|
|
```javascript
|
|
// ✅ CORRECT
|
|
const formData = new FormData();
|
|
formData.append('file', fileInput.files[0]);
|
|
await fetch(uploadURL, {
|
|
method: 'POST',
|
|
body: formData // Browser sets multipart/form-data automatically
|
|
});
|
|
|
|
// ❌ WRONG
|
|
await fetch(uploadURL, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }, // CORS error
|
|
body: JSON.stringify({ file: base64Image })
|
|
});
|
|
```
|
|
|
|
### Issue #2: Error 5408 - Upload Timeout
|
|
|
|
**Error**: `Error 5408` after ~15 seconds of upload
|
|
|
|
**Source**: [Cloudflare Community #571336](https://community.cloudflare.com/t/images-direct-creator-upload-error-5408/571336)
|
|
|
|
**Why It Happens**: Cloudflare has 30-second request timeout; slow uploads or large files exceed limit
|
|
|
|
**Prevention**:
|
|
- Compress images before upload (client-side with Canvas API)
|
|
- Use reasonable file size limits (e.g., max 10MB)
|
|
- Show upload progress to user
|
|
- Handle timeout errors gracefully
|
|
|
|
```javascript
|
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
alert('File too large. Please select an image under 10MB.');
|
|
return;
|
|
}
|
|
```
|
|
|
|
### Issue #3: Error 400 - Invalid File Parameter
|
|
|
|
**Error**: `400 Bad Request` with unhelpful error message
|
|
|
|
**Source**: [Cloudflare Community #487629](https://community.cloudflare.com/t/direct-creator-upload-returning-400/487629)
|
|
|
|
**Why It Happens**: File field must be named `file` (not `image`, `photo`, etc.)
|
|
|
|
**Prevention**:
|
|
```javascript
|
|
// ✅ CORRECT
|
|
formData.append('file', imageFile);
|
|
|
|
// ❌ WRONG
|
|
formData.append('image', imageFile); // 400 error
|
|
formData.append('photo', imageFile); // 400 error
|
|
```
|
|
|
|
### Issue #4: CORS Preflight Failures
|
|
|
|
**Error**: Preflight OPTIONS request blocked
|
|
|
|
**Source**: [Cloudflare Community #306805](https://community.cloudflare.com/t/cors-error-when-using-direct-creator-upload/306805)
|
|
|
|
**Why It Happens**: Calling `/direct_upload` API directly from browser (should be backend-only)
|
|
|
|
**Prevention**:
|
|
```
|
|
ARCHITECTURE:
|
|
Browser → Backend API → POST /direct_upload → Returns uploadURL → Browser uploads to uploadURL
|
|
```
|
|
|
|
Never expose API token to browser. Generate upload URL on backend, return to frontend.
|
|
|
|
### Issue #5: Error 9401 - Invalid Arguments
|
|
|
|
**Error**: `Cf-Resized: err=9401` - Required cf.image options missing or invalid
|
|
|
|
**Source**: [Cloudflare Images Docs - Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/)
|
|
|
|
**Why It Happens**: Missing required transformation parameters or invalid values
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// ✅ CORRECT
|
|
fetch(imageURL, {
|
|
cf: {
|
|
image: {
|
|
width: 800,
|
|
quality: 85,
|
|
format: 'auto'
|
|
}
|
|
}
|
|
});
|
|
|
|
// ❌ WRONG
|
|
fetch(imageURL, {
|
|
cf: {
|
|
image: {
|
|
width: 'large', // Must be number
|
|
quality: 150 // Max 100
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
### Issue #6: Error 9402 - Image Too Large
|
|
|
|
**Error**: `Cf-Resized: err=9402` - Image too large or connection interrupted
|
|
|
|
**Source**: [Cloudflare Images Docs - Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/)
|
|
|
|
**Why It Happens**: Image exceeds maximum area (100 megapixels) or download fails
|
|
|
|
**Prevention**:
|
|
- Validate image dimensions before transforming
|
|
- Use reasonable source images (max 10000x10000px)
|
|
- Handle network errors gracefully
|
|
|
|
### Issue #7: Error 9403 - Request Loop
|
|
|
|
**Error**: `Cf-Resized: err=9403` - Worker fetching its own URL or already-resized image
|
|
|
|
**Source**: [Cloudflare Images Docs - Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/)
|
|
|
|
**Why It Happens**: Transformation applied to already-transformed image, or Worker fetches itself
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// ✅ CORRECT
|
|
if (url.pathname.startsWith('/images/')) {
|
|
const originalPath = url.pathname.replace('/images/', '');
|
|
const originURL = `https://storage.example.com/${originalPath}`;
|
|
return fetch(originURL, { cf: { image: { width: 800 } } });
|
|
}
|
|
|
|
// ❌ WRONG
|
|
if (url.pathname.startsWith('/images/')) {
|
|
// Fetches worker's own URL, causes loop
|
|
return fetch(request, { cf: { image: { width: 800 } } });
|
|
}
|
|
```
|
|
|
|
### Issue #8: Error 9406/9419 - Invalid URL Format
|
|
|
|
**Error**: `Cf-Resized: err=9406` or `err=9419` - Non-HTTPS URL or URL has spaces/unescaped Unicode
|
|
|
|
**Source**: [Cloudflare Images Docs - Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/)
|
|
|
|
**Why It Happens**: Image URL uses HTTP (not HTTPS) or contains invalid characters
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// ✅ CORRECT
|
|
const imageURL = "https://example.com/images/photo%20name.jpg";
|
|
|
|
// ❌ WRONG
|
|
const imageURL = "http://example.com/images/photo.jpg"; // HTTP not allowed
|
|
const imageURL = "https://example.com/images/photo name.jpg"; // Space not encoded
|
|
```
|
|
|
|
Always use `encodeURIComponent()` for URL paths:
|
|
```typescript
|
|
const filename = "photo name.jpg";
|
|
const imageURL = `https://example.com/images/${encodeURIComponent(filename)}`;
|
|
```
|
|
|
|
### Issue #9: Error 9412 - Non-Image Response
|
|
|
|
**Error**: `Cf-Resized: err=9412` - Origin returned HTML instead of image
|
|
|
|
**Source**: [Cloudflare Images Docs - Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/)
|
|
|
|
**Why It Happens**: Origin server returns 404 page or error page (HTML) instead of image
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// Verify URL before transforming
|
|
const originResponse = await fetch(imageURL, { method: 'HEAD' });
|
|
const contentType = originResponse.headers.get('content-type');
|
|
|
|
if (!contentType?.startsWith('image/')) {
|
|
return new Response('Not an image', { status: 400 });
|
|
}
|
|
|
|
return fetch(imageURL, { cf: { image: { width: 800 } } });
|
|
```
|
|
|
|
### Issue #10: Error 9413 - Max Image Area Exceeded
|
|
|
|
**Error**: `Cf-Resized: err=9413` - Image exceeds 100 megapixels
|
|
|
|
**Source**: [Cloudflare Images Docs - Troubleshooting](https://developers.cloudflare.com/images/reference/troubleshooting/)
|
|
|
|
**Why It Happens**: Source image dimensions exceed 100 megapixels (e.g., 10000x10000px)
|
|
|
|
**Prevention**:
|
|
- Validate image dimensions before upload
|
|
- Pre-process oversized images
|
|
- Reject images above threshold
|
|
|
|
```typescript
|
|
const MAX_MEGAPIXELS = 100;
|
|
|
|
if (width * height > MAX_MEGAPIXELS * 1_000_000) {
|
|
return new Response('Image too large', { status: 413 });
|
|
}
|
|
```
|
|
|
|
### Issue #11: Flexible Variants + Signed URLs Incompatibility
|
|
|
|
**Error**: Flexible variants don't work with private images
|
|
|
|
**Source**: [Cloudflare Images Docs - Enable flexible variants](https://developers.cloudflare.com/images/manage-images/enable-flexible-variants/)
|
|
|
|
**Why It Happens**: Flexible variants cannot be used with `requireSignedURLs=true`
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// ✅ CORRECT - Use named variants for private images
|
|
await uploadImage({
|
|
file: imageFile,
|
|
requireSignedURLs: true // Use named variants: /public, /avatar, etc.
|
|
});
|
|
|
|
// ❌ WRONG - Flexible variants don't support signed URLs
|
|
// Cannot use: /w=400,sharpen=3 with requireSignedURLs=true
|
|
```
|
|
|
|
### Issue #12: SVG Resizing Limitation
|
|
|
|
**Error**: SVG files don't resize via transformations
|
|
|
|
**Source**: [Cloudflare Images Docs - SVG files](https://developers.cloudflare.com/images/transform-images/#svg-files)
|
|
|
|
**Why It Happens**: SVG is inherently scalable (vector format), resizing not applicable
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// SVGs can be served but not resized
|
|
// Use any variant name as placeholder
|
|
// https://imagedelivery.net/<HASH>/<SVG_ID>/public
|
|
|
|
// SVG will be served at original size regardless of variant settings
|
|
```
|
|
|
|
### Issue #13: EXIF Metadata Stripped by Default
|
|
|
|
**Error**: GPS data, camera settings removed from uploaded JPEGs
|
|
|
|
**Source**: [Cloudflare Images Docs - Transform via URL](https://developers.cloudflare.com/images/transform-images/transform-via-url/#metadata)
|
|
|
|
**Why It Happens**: Default behavior strips all metadata except copyright
|
|
|
|
**Prevention**:
|
|
```typescript
|
|
// Preserve metadata
|
|
fetch(imageURL, {
|
|
cf: {
|
|
image: {
|
|
width: 800,
|
|
metadata: 'keep' // Options: 'none', 'copyright', 'keep'
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
**Options**:
|
|
- `none`: Strip all metadata
|
|
- `copyright`: Keep only copyright tag (default for JPEG)
|
|
- `keep`: Preserve most EXIF metadata including GPS
|
|
|
|
---
|
|
|
|
## Using Bundled Resources
|
|
|
|
### Templates (templates/)
|
|
|
|
Copy-paste ready code for common patterns:
|
|
|
|
1. **wrangler-images-binding.jsonc** - Wrangler configuration (no binding needed)
|
|
2. **upload-api-basic.ts** - Upload file to Images API
|
|
3. **upload-via-url.ts** - Ingest image from external URL
|
|
4. **direct-creator-upload-backend.ts** - Generate one-time upload URLs
|
|
5. **direct-creator-upload-frontend.html** - User upload form
|
|
6. **transform-via-url.ts** - URL transformation examples
|
|
7. **transform-via-workers.ts** - Workers transformation patterns
|
|
8. **variants-management.ts** - Create/list/delete variants
|
|
9. **signed-urls-generation.ts** - HMAC-SHA256 signed URL generation
|
|
10. **responsive-images-srcset.html** - Responsive image patterns
|
|
11. **batch-upload.ts** - Batch API for high-volume uploads
|
|
|
|
**Usage**:
|
|
```bash
|
|
cp templates/upload-api-basic.ts src/upload.ts
|
|
# Edit with your account ID and API token
|
|
```
|
|
|
|
### References (references/)
|
|
|
|
In-depth documentation Claude can load as needed:
|
|
|
|
1. **api-reference.md** - Complete API endpoints (upload, list, delete, variants)
|
|
2. **transformation-options.md** - All transform params with examples
|
|
3. **variants-guide.md** - Named vs flexible variants, when to use each
|
|
4. **signed-urls-guide.md** - HMAC-SHA256 implementation details
|
|
5. **direct-upload-complete-workflow.md** - Full architecture and flow
|
|
6. **responsive-images-patterns.md** - srcset, sizes, art direction
|
|
7. **format-optimization.md** - WebP/AVIF auto-conversion strategies
|
|
8. **top-errors.md** - All 13+ errors with detailed troubleshooting
|
|
|
|
**When to load**:
|
|
- Deep-dive into specific feature
|
|
- Troubleshooting complex issues
|
|
- Understanding API details
|
|
- Implementing advanced patterns
|
|
|
|
### Scripts (scripts/)
|
|
|
|
**check-versions.sh** - Verify API endpoints are current
|
|
|
|
---
|
|
|
|
## Advanced Topics
|
|
|
|
**Custom Domains**: Serve from your domain via `/cdn-cgi/imagedelivery/<HASH>/<ID>/<VARIANT>` (requires domain on Cloudflare, proxied). Use Transform Rules for custom paths.
|
|
|
|
**Batch API**: High-volume uploads via `batch.imagedelivery.net` with batch tokens (Dashboard → Images → Batch API)
|
|
|
|
**Webhooks**: Notifications for Direct Creator Upload (Dashboard → Notifications → Webhooks). Payload includes `imageId`, `status`, `metadata`.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Problem: Images not transforming
|
|
|
|
**Symptoms**: `/cdn-cgi/image/...` returns original image or 404
|
|
|
|
**Solutions**:
|
|
1. Enable transformations on zone: Dashboard → Images → Transformations → Enable for zone
|
|
2. Verify zone is proxied through Cloudflare (orange cloud)
|
|
3. Check source image is publicly accessible
|
|
4. Wait 5-10 minutes for settings to propagate
|
|
|
|
### Problem: Direct upload returns CORS error
|
|
|
|
**Symptoms**: `Access-Control-Allow-Origin` error in browser console
|
|
|
|
**Solutions**:
|
|
1. Use `multipart/form-data` encoding (let browser set Content-Type)
|
|
2. Don't call `/direct_upload` from browser; call from backend
|
|
3. Name file field `file` (not `image`)
|
|
4. Remove manual Content-Type header
|
|
|
|
### Problem: Worker transformations return 9403 loop error
|
|
|
|
**Symptoms**: `Cf-Resized: err=9403` in response headers
|
|
|
|
**Solutions**:
|
|
1. Don't fetch Worker's own URL (use external origin)
|
|
2. Don't transform already-resized images
|
|
3. Check URL routing logic to avoid loops
|
|
|
|
### Problem: Signed URLs not working
|
|
|
|
**Symptoms**: 403 Forbidden when accessing signed URL
|
|
|
|
**Solutions**:
|
|
1. Verify image uploaded with `requireSignedURLs=true`
|
|
2. Check signature generation (HMAC-SHA256)
|
|
3. Ensure expiry timestamp is in future
|
|
4. Verify signing key matches dashboard (Images → Keys)
|
|
5. Cannot use flexible variants with signed URLs (use named variants)
|
|
|
|
### Problem: Images uploaded but not appearing
|
|
|
|
**Symptoms**: Upload returns 200 OK but image not in dashboard
|
|
|
|
**Solutions**:
|
|
1. Check for `draft: true` in response (Direct Creator Upload)
|
|
2. Wait for upload to complete (check via GET `/images/v1/{id}`)
|
|
3. Verify account ID matches
|
|
4. Check for upload errors in webhooks
|
|
|
|
---
|
|
|
|
## Official Documentation
|
|
|
|
- **Cloudflare Images**: https://developers.cloudflare.com/images/
|
|
- **Get Started**: https://developers.cloudflare.com/images/get-started/
|
|
- **Upload Images**: https://developers.cloudflare.com/images/upload-images/
|
|
- **Direct Creator Upload**: https://developers.cloudflare.com/images/upload-images/direct-creator-upload/
|
|
- **Transform Images**: https://developers.cloudflare.com/images/transform-images/
|
|
- **Transform via URL**: https://developers.cloudflare.com/images/transform-images/transform-via-url/
|
|
- **Transform via Workers**: https://developers.cloudflare.com/images/transform-images/transform-via-workers/
|
|
- **Create Variants**: https://developers.cloudflare.com/images/manage-images/create-variants/
|
|
- **Serve Private Images**: https://developers.cloudflare.com/images/manage-images/serve-images/serve-private-images/
|
|
- **Troubleshooting**: https://developers.cloudflare.com/images/reference/troubleshooting/
|
|
- **API Reference**: https://developers.cloudflare.com/api/resources/images/
|
|
|
|
---
|
|
|
|
## Package Versions
|
|
|
|
**Last Verified**: 2025-11-23
|
|
**API Version**: v2 (direct uploads), v1 (standard uploads)
|
|
**Optional**: @cloudflare/workers-types@4.20251121.0
|