Project six, image converter
Use R2 for originals, Cloudflare Images for supported transformations, and explicit parameter, source, cost, and cache boundaries.
Edited and verified by Orange Book Editorial Team ·
Completion criteria
A supported upload receives a stable object key; transformation accepts only approved sizes and formats; the Worker cannot become an arbitrary URL proxy; repeated transforms can be cached; oversized, non-image, missing-source, and transform-failure cases are explicit.
Prerequisites and companion example
Unit tests need no Cloudflare account:
node --test examples/image-transformer/test/index.test.mjsSix tests should pass, covering file signatures, server-generated object keys, preset mapping, arbitrary-URL rejection, loop protection, and downstream 404. A real transform additionally needs an R2 bucket, an HTTPS original-image origin you control, enabled Images Transformations, and that same host in allowed origins. Replace SOURCE_ORIGIN in wrangler.jsonc, then start:
cp examples/image-transformer/.dev.vars.example examples/image-transformer/.dev.vars
pnpm wrangler dev --config examples/image-transformer/wrangler.jsoncLocal preview and billing boundary
wrangler dev provides only a low-fidelity transform mock. Real Images transformations require account configuration, and each unique source/parameter combination can create billed usage. Do not aim automated tests at real transformations in bulk.
Two image paths
Cloudflare Images transformations can optimize remote images through a formatted URL or the cf.image options on a Worker fetch() and cache the transformed result. Use an Images binding when working with image bytes directly. R2 stores originals or generated objects that you own. These are separate responsibilities.
| Task | Suggested boundary |
|---|---|
| URL resize, crop, quality/format optimization | Images transformations |
| Read or write an original object | R2 binding |
| Validate user parameters and authorization | Worker |
| Large non-interactive batch | Queue plus background processor |
| Unsupported native codec | Evaluate a Container or external processor |
Safe transformation route
Never send an arbitrary user-provided url directly to fetch(). That can turn the utility into SSRF or a traffic proxy. Construct the source from a controlled object key and allowed origin, then accept only preset widths and fit behavior.
const allowedWidths = new Set([320, 640, 1280]);
const width = Number(new URL(request.url).searchParams.get('width') ?? 640);
if (!allowedWidths.has(width)) {
return Response.json({ error: 'invalid_width' }, { status: 400 });
}
const source = new URL(`/uploads/${safeObjectKey}`, 'https://media.example.com');
return fetch(source, {
cf: { image: { width, fit: 'scale-down' } },
});safeObjectKey comes from authentication, ownership, and path normalization. Replace the example domain with a controlled source configured for transformations. Confirm cf.image parameters against current primary documentation and local types.
Upload and transform flow
Accept an upload intent
Verify user, plan, declared file size, and allowed MIME first. Generate a server-owned object key and short-lived upload contract so a user cannot choose bucket paths or overwrite another user's object.
Verify the real object
After upload, read object metadata and check size, media type, and ownership. A public list returns only visible objects for the current tenant. An internal key is not authorization.
Apply transformation presets
Map thumb, card, and hero to server-owned dimensions and fit values instead of arbitrary combinations. Include a stable preset/version in output URLs for caching and future changes.
Handle failures
Return 404 for a missing source, 400 for invalid parameters, and stable authentication/authorization states. A downstream transform failure returns a retryable error without internal URLs or stack traces.
Observe cost and abuse
Track uploaded bytes, objects, and transform requests by user or tenant. Transformations are billed to the account that owns the Worker, so a public transform route needs per-path rate controls and application quotas.
Reading experience
Before and after
Preset cards
Job state
Accessibility
Verification and troubleshooting
| Symptom | Check first | Recovery |
|---|---|---|
Upload returns 415 | MIME matches the real file signature | Re-export JPEG/PNG/WebP/AVIF; do not rename an extension |
Transform returns invalid_source_origin | SOURCE_ORIGIN is HTTPS with no path, credentials, or query | Fix server configuration and restart the Worker |
Transform returns source_not_found | The controlled origin serves the returned object key | Check the R2 custom domain and object state |
Response is image_transform_loop | The transform route also intercepts originals | Separate /images/ from the original origin and keep Via protection |
Remote boundary and rollback
Enabling Images transformations, creating an R2 bucket, configuring a custom domain, and deploying rate rules change remote state. Code rollback cannot restore an overwritten or deleted original. Use immutable versioned object keys and delay physical deletion. If a new preset fails, restore the route mapping while preserving previous preset URLs so published pages do not lose images.
Next: establish the website and application defense baseline.
Primary sources
Did this page help you complete your goal?
Beta feedback is generated in this browser and is never uploaded automatically.