Storage & Data
Presigned R2 Upload
Generate presigned PUT URLs for direct browser-to-R2 uploads with aws4fetch - secure client uploads without Worker proxying
Generate presigned PUT URLs so browsers upload files directly to R2 without routing file bytes through the Worker. Includes a demo HTML form at GET /.
Features
- POST /presign - Returns a time-limited upload URL
- Direct browser upload - Client PUTs to R2 using the signed URL
- Content-type validation - Allowed types enforced at presign time
- Demo page -
GET /with file picker and upload flow
API Reference
POST /presign
Generate a presigned PUT URL for R2.
Prop
Type
Example Request
curl -X POST "https://your-worker.workers.dev/presign" \
-H "Content-Type: application/json" \
-d '{"filename":"photo.png","contentType":"image/png"}'Success Response
{
"uploadUrl": "https://account.r2.cloudflarestorage.com/bucket/uploads/uuid-photo.png?X-Amz-...",
"key": "uploads/550e8400-e29b-41d4-a716-446655440000-photo.png",
"contentType": "image/png",
"maxBytes": 5242880,
"expiresInSeconds": 900
}Upload the file with a matching Content-Type:
curl -X PUT "$uploadUrl" -H "Content-Type: image/png" --data-binary @photo.pngError Codes
400- Invalid JSON (INVALID_BODY), filename (INVALID_FILENAME), or content type (INVALID_CONTENT_TYPE)502- Presign failed, often missing R2 credentials (PRESIGN_ERROR)
GET /
Returns an HTML demo page with presign + direct PUT upload flow.
Use Cases
- Offload large file uploads from Workers to R2
- Learn aws4fetch presigned URL generation at the edge
- Prototype user-generated content uploads with size/type constraints
Limitations
- Requires R2 API credentials as Worker secrets (
R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY) - Browser uploads need R2 CORS configured for your origin
- Signed
Content-Typemust match the client PUT header exactly - Documented max size 5 MB (enforced at presign documentation level)
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/presigned-r2-upload.
DependenciesNoneBindingsR2PlatformFetch API
export const DEMO_HTML = `<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8" /> <title>Presigned R2 Upload</title> <style> body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; } label { display: block; margin: 1rem 0 0.25rem; } input, select, button { width: 100%; padding: 0.5rem; } pre { background: #f1f5f9; padding: 1rem; overflow: auto; } </style></head><body> <h1>Direct R2 Upload</h1> <p>Get a presigned URL, then PUT the file directly to R2 from your browser.</p> <label>Filename <input id="filename" value="demo.png" /></label> <label>Content-Type <select id="contentType"> <option value="image/png">image/png</option> <option value="image/jpeg">image/jpeg</option> <option value="text/plain">text/plain</option> <option value="application/pdf">application/pdf</option> </select> </label> <label>File <input id="file" type="file" /></label> <button id="upload">Upload via presigned URL</button> <pre id="log"></pre> <script> const log = document.getElementById('log'); document.getElementById('upload').onclick = async () => { const filename = document.getElementById('filename').value; const contentType = document.getElementById('contentType').value; const file = document.getElementById('file').files[0]; if (!file) { log.textContent = 'Choose a file first'; return; } const presign = await fetch('/presign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename, contentType }) }).then(r => r.json()); if (presign.error) { log.textContent = JSON.stringify(presign, null, 2); return; } const put = await fetch(presign.uploadUrl, { method: 'PUT', headers: { 'Content-Type': contentType }, body: file }); log.textContent = JSON.stringify({ presign, putStatus: put.status }, null, 2); }; </script></body></html>`;Deployment
Configure R2 and secrets
wrangler secret put R2_ACCESS_KEY_ID
wrangler secret put R2_SECRET_ACCESS_KEYSet R2_ACCOUNT_ID in wrangler.json vars. Configure R2 CORS for browser PUT uploads.
Test presign
curl -X POST "https://your-worker.workers.dev/presign" \
-H "Content-Type: application/json" \
-d '{"filename":"test.txt","contentType":"text/plain"}'Local Development
cd apps/experiments/presigned-r2-upload
npm install
npm run devOpen http://localhost:8787/ for the demo upload form.
Configuration
| Setting | Purpose |
|---|---|
UPLOADS | R2 bucket binding |
R2_ACCOUNT_ID | Account ID for signing (var) |
R2_ACCESS_KEY_ID | R2 API token access key (secret) |
R2_SECRET_ACCESS_KEY | R2 API token secret (secret) |