Token导航 LogoToken导航

Agent Skill · 研究检索

cloudflare-r2(Cloudflare R2 问题管理)

cloudflare-r2 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

cloudflare-r2 是研究检索类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为接触敏感数据;许可证为 MIT,第三方扫描有提醒项,按安装量在研究检索类的 73,675 个 Skill 里排第 11,687。

总安装 11,007周安装 468GitHub Stars 750下载 3,856核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
MIT

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:cloudflare-r2(Cloudflare R2 问题管理)
来源仓库:https://github.com/jezweb/claude-skills
仓库路径:skills/cloudflare-r2
安装命令:
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-r2
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-r2

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

Cloudflare 边缘上的 S3 兼容对象存储,具有 Workers API、分段上传、预签名 URL 和 CORS 支持。

通过 Workers 与 HTTP 元数据、自定义元数据和条件操作绑定来上传、下载和管理对象;支持范围请求和批量删除最多 1,000 个键 >100MB 的文件分段上传(5MB–100MB 部分,最多 10,000 个部分)和用于直接客户端访问的预签名 URL(仅限 S3 域,需要服务器端生成并过期) 使用 CLI 格式在存储桶设置中配置 CORS(规则 包装纸);预签名 URL 仅适用于 {bucket}.{account}.r2.cloudflarestorage.com ,不是自定义域 使用自定义域进行生产(切勿使用 r2.dev,它是有速率限制的);分片跨键写入以避免并发写入同一对象时出现 429 错误 防止 13 个已记录的问题,包括 CORS 格式混淆、缺少 contentType 、list() 元数据差距、API 令牌权限错误和平台中断故障;对 5xx 错误实施指数退避重试逻辑

来源仓库内路径:skills/cloudflare-r2

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-r2;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要接触敏感数据,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

cloudflare-r2 怎么安装?

在终端执行 npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-r2。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

cloudflare-r2 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

cloudflare-r2 需要什么权限?

权限级别是接触敏感数据。该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

cloudflare-r2 是免费的吗?

来源标注的许可证是 MIT。本站不代收任何费用,也不托管安装包;能否商用、要不要署名,以许可证原文为准。

cloudflare-r2 用的人多吗?

公开数据是累计安装 11,007 次,近一周安装 468 次,下载 3,856 次,GitHub 750 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

cloudflare-r2 安全吗?

第三方扫描有提醒项,说明存在需要人工确认的行为。安装前请打开源码看清它读了什么、联网到哪里。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 3 个目录同时收录,说明这个 Skill 流传比较广。
  • 许可证明确为 MIT,商用和分发边界好判断。

先想清楚的地方

  • 需要接触敏感数据,装到常用环境前要先评估影响面。
  • 第三方扫描存在提醒项或未通过项,需要人工逐条确认。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。

不太适合

  • 环境里有大量密钥、凭证,不方便给第三方 Skill 读取的人。
  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
find-skills

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
cloudflare-r2

Cloudflare R2 问题管理 · 本页

11,007750接触敏感数据MIT有提醒

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Cloudflare R2 Object Storage

Status: Production Ready ✅ Last Updated: 2026-01-20 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: wrangler@4.59.2, @cloudflare/workers-types@4.20260109.0, aws4fetch@1.0.20

Recent Updates (2025):

  • September 2025: R2 SQL open beta (serverless query engine for Apache Iceberg), Pipelines GA (real-time stream ingestion), Remote bindings GA (local dev connects to deployed R2)
  • May 2025: Dashboard redesign (deeplink support, bucket settings centralization), Super Slurper 5x faster (rebuilt with Workers/Queues/Durable Objects)
  • April 2025: R2 Data Catalog open beta (managed Apache Iceberg catalog), Event Notifications open beta (5,000 msg/s per Queue)
  • 2025: Bucket limits increased (1 million max), CRC-64/NVME checksums, Server-side encryption with customer keys, Infrequent Access storage class (beta), Oceania region, S3 API enhancements (sha256/sha1 checksums, ListParts, conditional CopyObject)

Quick Start (5 Minutes)

# 1. Create bucket
npx wrangler r2 bucket create my-bucket

# 2. Add binding to wrangler.jsonc
# {
#   "r2_buckets": [{
#     "binding": "MY_BUCKET",
#     "bucket_name": "my-bucket",
#     "preview_bucket_name": "my-bucket-preview"  // Optional: separate dev/prod
#   }]
# }

# 3. Upload/download from Worker
type Bindings = { MY_BUCKET: R2Bucket };

// Upload
await env.MY_BUCKET.put('file.txt', data, {
  httpMetadata: { contentType: 'text/plain' }
});

// Download
const object = await env.MY_BUCKET.get('file.txt');
if (!object) return c.json({ error: 'Not found' }, 404);

return new Response(object.body, {
  headers: {
    'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
    'ETag': object.httpEtag,
  },
});

# 4. Deploy
npx wrangler deploy

R2 Workers API

Core Methods

// put() - Upload objects
await env.MY_BUCKET.put('file.txt', data, {
  httpMetadata: {
    contentType: 'text/plain',
    cacheControl: 'public, max-age=3600',
  },
  customMetadata: { userId: '123' },
  md5: await crypto.subtle.digest('MD5', data),  // Checksum verification
});

// Conditional upload (prevent overwrites)
const object = await env.MY_BUCKET.put('file.txt', data, {
  onlyIf: { uploadedBefore: new Date('2020-01-01') }
});
if (!object) return c.json({ error: 'File already exists' }, 409);

// get() - Download objects
const object = await env.MY_BUCKET.get('file.txt');
if (!object) return c.json({ error: 'Not found' }, 404);

const text = await object.text();           // As string
const json = await object.json();           // As JSON
const buffer = await object.arrayBuffer();  // As ArrayBuffer

// Range requests (partial downloads)
const partial = await env.MY_BUCKET.get('video.mp4', {
  range: { offset: 0, length: 1024 * 1024 }  // First 1MB
});

// head() - Get metadata only (no body download)
const object = await env.MY_BUCKET.head('file.txt');
console.log(object.size, object.etag, object.customMetadata);

// delete() - Delete objects
await env.MY_BUCKET.delete('file.txt');  // Single delete (idempotent)
await env.MY_BUCKET.delete(['file1.txt', 'file2.txt']);  // Bulk delete (max 1000)

// list() - List objects
const listed = await env.MY_BUCKET.list({
  prefix: 'images/',  // Filter by prefix
  limit: 100,
  cursor: cursor,     // Pagination
  delimiter: '/',     // Folder-like listing
  include: ['httpMetadata', 'customMetadata'],  // IMPORTANT: Opt-in for metadata
});

for (const object of listed.objects) {
  console.log(`${object.key}: ${object.size} bytes`);
  console.log(object.httpMetadata?.contentType);  // Now populated with include parameter
  console.log(object.customMetadata);             // Now populated with include parameter
}

Multipart Uploads

For files >100MB or resumable uploads. Use when: large files, browser uploads, parallelization needed.

// 1. Create multipart upload
const multipart = await env.MY_BUCKET.createMultipartUpload('large-file.zip', {
  httpMetadata: { contentType: 'application/zip' }
});

// 2. Upload parts (5MB-100MB each, max 10,000 parts)
const multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
const part1 = await multipart.uploadPart(1, chunk1);
const part2 = await multipart.uploadPart(2, chunk2);

// 3. Complete upload
const object = await multipart.complete([
  { partNumber: 1, etag: part1.etag },
  { partNumber: 2, etag: part2.etag },
]);

// 4. Abort if needed
await multipart.abort();

Limits: Parts 5MB-100MB, max 10,000 parts per upload. Don't use for files <5MB (overhead).


Presigned URLs

Allow clients to upload/download directly to/from R2 (bypasses Worker). Use aws4fetch library.

import { AwsClient } from 'aws4fetch';

const r2Client = new AwsClient({
  accessKeyId: env.R2_ACCESS_KEY_ID,
  secretAccessKey: env.R2_SECRET_ACCESS_KEY,
});

const url = new URL(
  `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/${filename}`
);
url.searchParams.set('X-Amz-Expires', '3600');  // 1 hour expiry

const signed = await r2Client.sign(
  new Request(url, { method: 'PUT' }),  // or 'GET' for downloads
  { aws: { signQuery: true } }
);

// Client uploads directly to R2
await fetch(signed.url, { method: 'PUT', body: file });

CRITICAL Security:

  • ❌ NEVER expose R2 access keys in client-side code
  • ✅ ALWAYS generate presigned URLs server-side
  • ✅ ALWAYS set expiry times (1-24 hours typical)
  • ✅ ALWAYS add authentication before generating URLs
  • ✅ CONSIDER scoping to user folders: users/${userId}/${filename}

Presigned URL Domain Requirements

CRITICAL: Presigned URLs ONLY work with the S3 API domain, not custom domains.

// ❌ WRONG - Presigned URLs don't work with custom domains
const url = new URL(`https://cdn.example.com/${filename}`);
const signed = await r2Client.sign(
  new Request(url, { method: 'PUT' }),
  { aws: { signQuery: true } }
);
// This URL will fail - presigning requires S3 domain

// ✅ CORRECT - Use R2 storage domain for presigned URLs
const url = new URL(
  `https://${accountId}.r2.cloudflarestorage.com/${filename}`
);
const signed = await r2Client.sign(
  new Request(url, { method: 'PUT' }),
  { aws: { signQuery: true } }
);

// Pattern: Upload via presigned S3 URL, serve via custom domain
async function generateUploadUrl(filename: string) {
  const uploadUrl = new URL(
    `https://${accountId}.r2.cloudflarestorage.com/${filename}`
  );
  const signed = await r2Client.sign(
    new Request(uploadUrl, { method: 'PUT' }),
    { aws: { signQuery: true } }
  );

  return {
    uploadUrl: signed.url,  // For client upload (S3 domain)
    publicUrl: `https://cdn.example.com/${filename}`  // For serving (custom domain)
  };
}

Source: Community Knowledge

API Token Requirements for Wrangler

⚠️ Wrangler CLI requires "Admin Read & Write" permissions, not "Object Read & Write".

When creating API tokens for wrangler operations:

  • ✅ Use: R2 → Admin Read & Write
  • ❌ Don't use: R2 → Object Read & Write (causes 403 Forbidden errors)

Why: "Object Read & Write" is for S3 API direct access only. Wrangler needs admin-level permissions for bucket operations.

# With wrong permissions:
export CLOUDFLARE_API_TOKEN="token_with_object_readwrite"
wrangler r2 object put my-bucket/file.txt --file=./file.txt --remote
# ✘ [ERROR] Failed to fetch - 403: Forbidden

# With correct permissions (Admin Read & Write):
wrangler r2 object put my-bucket/file.txt --file=./file.txt --remote
# ✔ Success

Source: GitHub Issue #9235


CORS Configuration

Configure CORS in bucket settings (Dashboard → R2 → Bucket → Settings → CORS Policy) before browser access.

Dashboard Format vs CLI Format

⚠️ The wrangler CLI and Dashboard UI use DIFFERENT CORS formats. This commonly causes confusion.

Dashboard Format (works in UI only):

[{
  "AllowedOrigins": ["https://example.com"],
  "AllowedMethods": ["GET", "PUT"],
  "AllowedHeaders": ["*"],
  "ExposeHeaders": ["ETag"],
  "MaxAgeSeconds": 3600
}]

CLI Format (required for wrangler r2 bucket cors):

{
  "rules": [{
    "allowed": {
      "origins": ["https://www.example.com"],
      "methods": ["GET", "PUT"],
      "headers": ["Content-Type", "Authorization"]
    },
    "exposeHeaders": ["ETag", "Content-Length"],
    "maxAgeSeconds": 8640
  }]
}
# Using CLI format
wrangler r2 bucket cors set my-bucket --file cors-config.json

# Error if using Dashboard format:
# "The CORS configuration file must contain a 'rules' array"

Source: GitHub Issue #10076

Custom Domain CORS

When using custom domains with R2, CORS is handled in two layers:

  1. R2 Bucket CORS: Applies to all access methods (presigned URLs, direct S3 access)
  2. Transform Rules CORS: Additional CORS headers via Cloudflare Cache settings on custom domain
// Bucket CORS (set via dashboard or wrangler)
{
  "rules": [{
    "allowed": {
      "origins": ["https://app.example.com"],
      "methods": ["GET", "PUT"],
      "headers": ["Content-Type"]
    },
    "maxAgeSeconds": 3600
  }]
}

// Additional CORS via Transform Rules (Dashboard → Rules → Transform Rules)
// Modify Response Header: Access-Control-Allow-Origin: https://app.example.com

// Order of CORS evaluation:
// 1. R2 bucket CORS (if presigned URL or direct R2 access)
// 2. Transform Rules CORS (if via custom domain)

Source: Community Knowledge

For presigned URLs: CORS handled by R2 directly (configure on bucket, not Worker).


HTTP Metadata & Custom Metadata

// HTTP metadata (standard headers)
await env.MY_BUCKET.put('file.pdf', data, {
  httpMetadata: {
    contentType: 'application/pdf',
    cacheControl: 'public, max-age=31536000, immutable',
    contentDisposition: 'attachment; filename="report.pdf"',
    contentEncoding: 'gzip',
  },
  customMetadata: {
    userId: '12345',
    version: '1.0',
  }  // Max 2KB total, keys/values must be strings
});

// Read metadata
const object = await env.MY_BUCKET.head('file.pdf');
console.log(object.httpMetadata, object.customMetadata);

Error Handling

Common R2 Errors

try {
  await env.MY_BUCKET.put(key, data);
} catch (error: any) {
  const message = error.message;

  if (message.includes('R2_ERROR')) {
    // Generic R2 error
  } else if (message.includes('exceeded')) {
    // Quota exceeded
  } else if (message.includes('precondition')) {
    // Conditional operation failed
  } else if (message.includes('multipart')) {
    // Multipart upload error
  }

  console.error('R2 Error:', message);
  return c.json({ error: 'Storage operation failed' }, 500);
}

Retry Logic

R2 experienced two major outages in Q1 2025 (February 6: 59 minutes, March 21: 1h 7min) due to operational issues. Implement robust retry logic with exponential backoff for platform errors.

async function r2WithRetry<T>(
  operation: () => Promise<T>,
  maxRetries = 5
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: any) {
      const message = error.message;

      // Retry on transient errors and platform issues
      const is5xxError =
        message.includes('500') ||
        message.includes('502') ||
        message.includes('503') ||
        message.includes('504');

      const isRetryable =
        is5xxError ||
        message.includes('network') ||
        message.includes('timeout') ||
        message.includes('temporarily unavailable');

      if (!isRetryable || attempt === maxRetries - 1) {
        throw error;
      }

      // Exponential backoff (longer for platform errors)
      // 5xx errors: 1s, 2s, 4s, 8s, 16s (up to 31s total)
      // Other errors: 1s, 2s, 4s, 5s, 5s (up to 17s total)
      const delay = is5xxError
        ? Math.min(1000 * Math.pow(2, attempt), 16000)
        : Math.min(1000 * Math.pow(2, attempt), 5000);

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error('Max retries exceeded');
}

// Usage
const object = await r2WithRetry(() =>
  env.MY_BUCKET.get('important-file.txt')
);

Platform Reliability: While R2 is generally reliable, the 2025 Q1 outages demonstrate the importance of retry logic for production applications. All 5xx errors should be retried with exponential backoff.

Sources:


Performance Optimization

// Batch delete (up to 1000 keys)
await env.MY_BUCKET.delete(['file1.txt', 'file2.txt', 'file3.txt']);

// Range requests for large files
const partial = await env.MY_BUCKET.get('video.mp4', {
  range: { offset: 0, length: 10 * 1024 * 1024 }  // First 10MB
});

// Cache headers for immutable assets
await env.MY_BUCKET.put('static/app.abc123.js', jsData, {
  httpMetadata: { cacheControl: 'public, max-age=31536000, immutable' }
});

// Checksums for data integrity
const md5Hash = await crypto.subtle.digest('MD5', fileData);
await env.MY_BUCKET.put('important.dat', fileData, { md5: md5Hash });

Concurrent Write Rate Limits

⚠️ High-frequency concurrent writes to the same object key will trigger HTTP 429 rate limiting.

// ❌ BAD: Multiple Workers writing to same key rapidly
async function logToSharedFile(env: Env, logEntry: string) {
  const existing = await env.LOGS.get('global-log.txt');
  const content = (await existing?.text()) || '';
  await env.LOGS.put('global-log.txt', content + logEntry);
  // High write frequency to same key = 429 errors
}

// ✅ GOOD: Shard by timestamp or ID (distribute writes)
async function logWithSharding(env: Env, logEntry: string) {
  const timestamp = Date.now();
  const shard = Math.floor(timestamp / 60000); // 1-minute shards

  await env.LOGS.put(`logs/${shard}.txt`, logEntry, {
    customMetadata: { timestamp: timestamp.toString() }
  });
  // Different keys = no rate limiting
}

// ✅ ALTERNATIVE: Use Durable Objects for append operations
// Durable Objects can handle high-frequency updates to same state

// ✅ ALTERNATIVE: Use Queues + batch processing
// Buffer writes and batch them with unique keys

Source: R2 Limits Documentation

R2.dev Domain Rate Limiting

🚨 CRITICAL: The {bucket}.{account}.r2.cloudflarestorage.com (r2.dev) domain is NOT for production use.

r2.dev limitations:

  • ❌ Variable rate limiting (starts at ~hundreds of requests/second)
  • ❌ Bandwidth throttling
  • ❌ No SLA or performance guarantees
  • ❌ You'll receive 429 Too Many Requests under load

For production: ALWAYS use custom domains

// ❌ NOT for production - r2.dev endpoint
const publicUrl = `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/${key}`;
// This will be rate limited in production

// ✅ Production: Custom domain
const productionUrl = `https://cdn.example.com/${key}`;

// Setup custom domain:
// 1. Dashboard → R2 → Bucket → Settings → Custom Domains
// 2. Add your domain (e.g., cdn.example.com)
// 3. Benefits:
//    - No rate limiting beyond account limits
//    - Cloudflare Cache support
//    - Custom cache rules via Workers
//    - Full CDN features

r2.dev is ONLY for testing/development. Custom domains are required for production.

Source: R2 Limits Documentation


Best Practices Summary

Always Do:

  • Set contentType for all uploads
  • Use batch delete for multiple objects (up to 1000)
  • Set cache headers for static assets
  • Use presigned URLs for large client uploads (S3 domain only)
  • Use multipart for files >100MB
  • Set CORS before browser uploads (use CLI format for wrangler)
  • Set expiry times on presigned URLs (1-24 hours)
  • Use head() when you only need metadata
  • Use conditional operations to prevent overwrites
  • Use custom domains for production (never r2.dev)
  • Shard writes across keys to avoid rate limits
  • Use include parameter with list() to get metadata
  • Implement retry logic with exponential backoff for 5xx errors

Never Do:

  • Never expose R2 access keys in client-side code
  • Never skip contentType (files download as binary)
  • Never delete in loops (use batch delete)
  • Never skip CORS for browser uploads
  • Never use multipart for small files (<5MB)
  • Never delete >1000 keys in single call
  • Never skip presigned URL expiry (security risk)
  • Never use r2.dev domain for production (rate limited)
  • Never use presigned URLs with custom domains (use S3 domain)
  • Never write to same key at high frequency (causes 429)
  • Never use "Object Read & Write" tokens for wrangler (use "Admin Read & Write")

Multi-Tenant Architecture

With the bucket limit increased to 1 million buckets per account, per-tenant buckets are now viable for large-scale applications.

// Option 1: Per-tenant buckets (now scalable to 1M tenants)
const bucketName = `tenant-${tenantId}`;
const bucket = env[bucketName]; // Dynamic binding

// Option 2: Key prefixing (still preferred for most use cases)
await env.MY_BUCKET.put(`tenants/${tenantId}/file.txt`, data);

// Choose based on:
// - Per-tenant buckets: Strong isolation, separate billing/quotas
// - Key prefixing: Simpler, fewer resources, easier to manage

Source: R2 Limits Documentation


Known Issues Prevented

This skill prevents 13 documented issues:

Issue #IssueErrorPrevention
#1CORS errors in browserBrowser can't upload/downloadConfigure CORS in bucket settings, use correct CLI format
#2Files download as binaryMissing content-typeAlways set httpMetadata.contentType on upload
#3Presigned URL expiryURLs never expireAlways set X-Amz-Expires (1-24 hours)
#4Multipart upload limitsParts exceed limitsKeep parts 5MB-100MB, max 10,000 parts
#5Bulk delete limits>1000 keys failsChunk deletes into batches of 1000
#6Custom metadata overflowExceeds 2KB limitKeep custom metadata under 2KB
#7list() metadata missinghttpMetadata undefinedUse include: ['httpMetadata', 'customMetadata'] parameter (Issue #10870)
#8CORS format confusion"Must contain 'rules' array"Use CLI format with rules wrapper for wrangler (Issue #10076)
#9API token 403 errors"Failed to fetch - 403"Use "Admin Read & Write" not "Object Read & Write" for wrangler (Issue #9235)
#10r2.dev rate limitingHTTP 429 in productionUse custom domains, never r2.dev for production (R2 Limits)
#11Concurrent write 429sSame key written frequentlyShard writes across different keys (R2 Limits)
#12Presigned URL domain errorPresigned URLs failUse S3 domain only, not custom domains (Community)
#13Platform outages5xx errors during outagesImplement retry logic with exponential backoff (Feb 6, Mar 21)

Development Best Practices

Local R2 Storage Cleanup

⚠️ Local R2 DELETE operations don't cleanup blob files. When using wrangler dev, deleted objects remain in .wrangler/state/v3/r2/{bucket-name}/blobs/, causing local storage to grow indefinitely.

# Symptom: .wrangler/state grows large during development
du -sh .wrangler/state/v3/r2/

# Fix: Manually cleanup local R2 storage
rm -rf .wrangler/state/v3/r2/

# Alternative: Use remote R2 for development
wrangler dev --remote

Source: GitHub Issue #10795

Remote R2 Access Issues

⚠️ Local dev with --remote can have unreliable .get() operations. Some users report get() returning undefined despite put() working correctly.

# If experiencing issues with remote R2 in local dev:

# Option 1: Use local buckets instead (recommended)
wrangler dev  # No --remote flag

# Option 2: Deploy to preview environment for testing
wrangler deploy --env preview

# Option 3: Add retry logic if must use --remote
async function safeGet(bucket: R2Bucket, key: string) {
  for (let i = 0; i < 3; i++) {
    const obj = await bucket.get(key);
    if (obj && obj.body) return obj;
    await new Promise(r => setTimeout(r, 1000));
  }
  throw new Error('Failed to get object after retries');
}

Source: GitHub Issue #8868 (Community-sourced)


Wrangler Commands Reference

# Bucket management
wrangler r2 bucket create <BUCKET_NAME>
wrangler r2 bucket list
wrangler r2 bucket delete <BUCKET_NAME>

# Object management
wrangler r2 object put <BUCKET_NAME>/<KEY> --file=<FILE_PATH>
wrangler r2 object get <BUCKET_NAME>/<KEY> --file=<OUTPUT_PATH>
wrangler r2 object delete <BUCKET_NAME>/<KEY>

# List objects
wrangler r2 object list <BUCKET_NAME>
wrangler r2 object list <BUCKET_NAME> --prefix="folder/"

Official Documentation


Ready to store with R2! 🚀

Last verified: 2026-01-20 | Skill version: 2.0.0 | Changes: Added 7 new known issues from community research (list() metadata, CORS format confusion, API token permissions, r2.dev rate limiting, concurrent write limits, presigned URL domain requirements, platform outage retry patterns). Enhanced retry logic for 5xx errors, added development best practices section, documented bucket limit increase to 1M.

平台分布

Claude Code

25.22%
按下载量换算972

Gemini CLI

20.9%
按下载量换算806

Antigravity

19.31%
按下载量换算745

OpenCode

12.11%
按下载量换算467

Cursor

7.73%
按下载量换算298

Codex

3.74%
按下载量换算144

安全审计

Gen Agent Trust Hub

可疑

Socket

通过

Snyk

通过

权限和风险

接触敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在提醒或未通过项,安装前请自行复核。

继续浏览同类 Skills