Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Cloudflare platform skill covering Workers, D1, R2, KV, AI, Durable Objects, and security.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/r2/gotchas.md
1# R2 Gotchas & Troubleshooting23## List Truncation45```typescript6// ❌ WRONG: Don't compare object count when using include7while (listed.objects.length < options.limit) { ... }89// ✅ CORRECT: Always use truncated property10while (listed.truncated) {11const next = await env.MY_BUCKET.list({ cursor: listed.cursor });12// ...13}14```1516**Reason:** `include` with metadata may return fewer objects per page to fit metadata.1718## ETag Format1920```typescript21// ❌ WRONG: Using etag (unquoted) in headers22headers.set('etag', object.etag); // Missing quotes2324// ✅ CORRECT: Use httpEtag (quoted)25headers.set('etag', object.httpEtag);26```2728## Checksum Limits2930Only ONE checksum algorithm allowed per PUT:3132```typescript33// ❌ WRONG: Multiple checksums34await env.MY_BUCKET.put(key, data, { md5: hash1, sha256: hash2 }); // Error3536// ✅ CORRECT: Pick one37await env.MY_BUCKET.put(key, data, { sha256: hash });38```3940## Multipart Requirements4142- All parts must be uniform size (except last part)43- Part numbers start at 1 (not 0)44- Uncompleted uploads auto-abort after 7 days45- `resumeMultipartUpload` doesn't validate uploadId existence4647## Conditional Operations4849```typescript50// Precondition failure returns object WITHOUT body51const object = await env.MY_BUCKET.get(key, {52onlyIf: { etagMatches: '"wrong"' }53});5455// Check for body, not just null56if (!object) return new Response('Not found', { status: 404 });57if (!object.body) return new Response(null, { status: 304 }); // Precondition failed58```5960## Key Validation6162```typescript63// ❌ DANGEROUS: Path traversal64const key = url.pathname.slice(1); // Could be ../../../etc/passwd65await env.MY_BUCKET.get(key);6667// ✅ SAFE: Validate keys68if (!key || key.includes('..') || key.startsWith('/')) {69return new Response('Invalid key', { status: 400 });70}71```7273## Storage Class Pitfalls7475- InfrequentAccess: 30-day minimum billing (even if deleted early)76- Can't transition IA → Standard via lifecycle (use S3 CopyObject)77- Retrieval fees apply for IA reads7879## Stream Length Requirement8081```typescript82// ❌ WRONG: Streaming unknown length fails silently83const response = await fetch(url);84await env.MY_BUCKET.put(key, response.body); // May fail without error8586// ✅ CORRECT: Buffer or use Content-Length87const data = await response.arrayBuffer();88await env.MY_BUCKET.put(key, data);8990// OR: Pass Content-Length if known91const object = await env.MY_BUCKET.put(key, request.body, {92httpMetadata: {93contentLength: parseInt(request.headers.get('content-length') || '0')94}95});96```9798**Reason:** R2 requires known length for streams. Unknown length may cause silent truncation.99100## S3 SDK Region Configuration101102```typescript103// ❌ WRONG: Missing region breaks ALL S3 SDK calls104const s3 = new S3Client({105endpoint: `https://${accountId}.r2.cloudflarestorage.com`,106credentials: { ... }107});108109// ✅ CORRECT: MUST set region='auto'110const s3 = new S3Client({111region: 'auto', // REQUIRED112endpoint: `https://${accountId}.r2.cloudflarestorage.com`,113credentials: { ... }114});115```116117**Reason:** S3 SDK requires region. R2 uses 'auto' as placeholder.118119## Local Development Limits120121```typescript122// ❌ Miniflare/wrangler dev: Limited R2 support123// - No multipart uploads124// - No presigned URLs (requires S3 SDK + network)125// - Memory-backed storage (lost on restart)126127// ✅ Use remote bindings for full features128wrangler dev --remote129130// OR: Conditional logic131if (env.ENVIRONMENT === 'development') {132// Fallback for local dev133} else {134// Full R2 features135}136```137138## Presigned URL Expiry139140```typescript141// ❌ WRONG: URL expires but no client validation142const url = await getSignedUrl(s3, command, { expiresIn: 60 });143// 61 seconds later: 403 Forbidden144145// ✅ CORRECT: Return expiry to client146return Response.json({147uploadUrl: url,148expiresAt: new Date(Date.now() + 60000).toISOString()149});150```151152## Limits153154| Limit | Value |155|-------|-------|156| Object size | 5 TB |157| Multipart part count | 10,000 |158| Multipart part min size | 5 MB (except last) |159| Batch delete | 1,000 keys |160| List limit | 1,000 per request |161| Key size | 1024 bytes |162| Custom metadata | 2 KB per object |163| Presigned URL max expiry | 7 days |164165## Common Errors166167### "Stream upload failed" / Silent Truncation168169**Cause:** Stream length unknown or Content-Length missing170**Solution:** Buffer data or pass explicit Content-Length171172### "Invalid credentials" / S3 SDK173174**Cause:** Missing `region: 'auto'` in S3Client config175**Solution:** Always set `region: 'auto'` for R2176177### "Object not found"178179**Cause:** Object key doesn't exist or was deleted180**Solution:** Verify object key correct, check if object was deleted, ensure bucket correct181182### "List compatibility error"183184**Cause:** Missing or old compatibility_date, or flag not enabled185**Solution:** Set `compatibility_date >= 2022-08-04` or enable `r2_list_honor_include` flag186187### "Multipart upload failed"188189**Cause:** Part sizes not uniform or incorrect part number190**Solution:** Ensure uniform size except final part, verify part numbers start at 1191