Files
2025-11-30 08:24:18 +08:00

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