Skip to content

Workers R2 -- CORS Configuration

DodaTech 5 min read

In this tutorial, you will learn how to configure CORS rules for Cloudflare R2 buckets so that browser-based applications from your domains can read and write objects without being blocked by same-origin policy. CORS configuration is important because browsers enforce strict cross-origin restrictions, and R2 buckets are served from a different origin than your application domain. A real-world example is a single-page application at app.example.com that uploads user profile photos directly to an R2 bucket at assets.example.com.

Understanding CORS for R2

CORS (Cross-Origin Resource Sharing) uses HTTP headers to tell browsers whether a web application running at one origin can access resources from a different origin. For R2, you configure CORS rules at the bucket level. These rules specify which origins are allowed, which HTTP methods are permitted, which headers the browser can send, and whether the browser can include credentials. Without proper CORS, browser fetch requests to R2 fail with opaque responses or CORS errors.

CORS Rule Structure

flowchart TD
    B[Browser at app.example.com] -->|fetch with Origin header| R[R2 Bucket]
    R -->|Returns Access-Control-Allow-Origin| B
    B -->|Preflight OPTIONS request| R
    R -->|Returns allowed methods + headers| B
    B -->|Actual GET/PUT request| R
    R -->|Object data| B

    subgraph CORS Config
        O[AllowedOrigins]
        M[AllowedMethods]
        H[AllowedHeaders]
        E[ExposeHeaders]
        A[MaxAgeSeconds]
    end

    style R fill:#f90,color:#fff
    style O fill:#2ecc71,color:#fff
    style M fill:#3498db,color:#fff

Each CORS rule can match one or more origins. The browser sends an Origin header, and R2 responds with the matching Access-Control-Allow-Origin header if the origin is in the allowed list.

Setting CORS via Wrangler

# Apply CORS rules to an R2 bucket
npx wrangler r2 bucket cors set my-bucket --rules '[
  {
    "AllowedOrigins": ["https://app.example.com", "https://admin.example.com"],
    "AllowedMethods": ["GET", "PUT", "DELETE", "POST"],
    "AllowedHeaders": ["Content-Type", "Authorization", "x-custom-header"],
    "ExposeHeaders": ["ETag", "x-amz-request-id"],
    "MaxAgeSeconds": 3600
  }
]'

# Expected output:
# Updating CORS rules for bucket my-bucket...
# CORS rules updated successfully

The AllowedOrigins field specifies exact origins including protocol and port. Use * to allow all origins, but this disables credentials and is not recommended for production.

Setting CORS via S3 API

// Using AWS SDK for JavaScript with R2 endpoint
import { S3Client, PutBucketCorsCommand } from '@aws-sdk/client-s3';

const client = new S3Client({
  region: 'auto',
  endpoint: 'https://<account-id>.r2.cloudflarestorage.com',
  credentials: {
    accessKeyId: '<access-key-id>',
    secretAccessKey: '<secret-access-key>'
  }
});

const command = new PutBucketCorsCommand({
  Bucket: 'my-bucket',
  CORSConfiguration: {
    CORSRules: [
      {
        AllowedOrigins: ['https://app.example.com'],
        AllowedMethods: ['GET', 'PUT'],
        AllowedHeaders: ['Content-Type', 'Authorization'],
        ExposeHeaders: ['ETag'],
        MaxAgeSeconds: 3600
      }
    ]
  }
});

const response = await client.send(command);
console.log(response);

// Expected output:
// {
//   '$metadata': { httpStatusCode: 200, attempts: 1, totalRetryDelay: 0 }
// }

The S3 PutBucketCors API works identically with R2. The rules are stored as JSON on the bucket and applied to every subsequent request.

Testing CORS with a Worker

// Test CORS from a Worker -- craft a cross-origin request
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.searchParams.get('key') || 'test-file.txt';

    // Simulate a browser making a cross-origin request
    const testRequest = new Request('https://my-bucket.r2.dev/' + key, {
      method: 'GET',
      headers: {
        'Origin': 'https://app.example.com'
      }
    });

    const response = await fetch(testRequest);

    return new Response(JSON.stringify({
      status: response.status,
      corsHeader: response.headers.get('Access-Control-Allow-Origin'),
      allowedMethods: response.headers.get('Access-Control-Allow-Methods'),
      etag: response.headers.get('ETag')
    }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

// Expected response:
// {"status": 200, "corsHeader": "https://app.example.com", "allowedMethods": null, "etag": "\"abc123\""}

The CORS headers appear on the response only when the browser includes an Origin header that matches a configured rule. The Access-Control-Allow-Origin header mirrors the request origin.

Handling Preflight Requests

// Worker that handles OPTIONS preflight for buckets without CORS config
export default {
  async fetch(request, env) {
    if (request.method === 'OPTIONS') {
      // Handle preflight directly in Worker
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
          'Access-Control-Max-Age': '86400'
        }
      });
    }

    // Normal object operations
    const url = new URL(request.url);
    const key = url.pathname.slice(1);
    const object = await env.BUCKET.get(key);

    if (!object) return new Response('Not found', { status: 404 });

    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set('Access-Control-Allow-Origin', '*');

    return new Response(object.body, { headers });
  }
};

// Preflight response expected for OPTIONS request with Origin: https://app.example.com:
// Status 204 with Access-Control-Allow-Origin: *

When a browser sends a preflight OPTIONS request, R2 with CORS configured returns the allowed methods and headers. Without bucket-level CORS, you can handle preflight in a Worker that sits in front of R2.

Common Errors and Troubleshooting

CORS Header Missing

If the Access-Control-Allow-Origin header is absent in the response, the CORS rule does not match the request origin. Verify the origin exactly matches including protocol, host, and port. https://app.example.com and https://app.example.com:443 are considered different origins.

Preflight Fails with 403

Some R2 configurations return 403 on OPTIONS requests if no matching CORS rule exists. Add an OPTIONS handler in your Worker or configure CORS rules on the bucket.

Asterisk Origin with Credentials

Setting AllowedOrigins to * prevents the browser from sending credentials (cookies, Authorization headers). For credentialed requests, list specific origins explicitly.

CORS Cache Poisoning

CORS preflight responses are cached by the browser for the duration of MaxAgeSeconds. If you change CORS rules, browsers may continue using cached preflight results. Set a conservative MaxAge such as 3600 seconds during development.

Multiple Origin Headers

If a request contains multiple Origin headers, R2 returns an error. Browsers send only one Origin header, but custom scripts may send duplicates. Strip duplicate Origin headers before forwarding.

Practice Questions

  1. What HTTP method does a browser send as a CORS preflight request?
  2. Which field in a CORS rule specifies which websites can access the bucket?
  3. Why should you avoid using * for AllowedOrigins in production?

FAQ

Can I configure CORS for R2 through the Cloudflare dashboard?

Yes. Navigate to R2 in the Cloudflare Dashboard, select your bucket, and open the Settings tab. The CORS section provides a form-based editor for adding, editing, and deleting CORS rules. Changes take effect immediately.

Does CORS configuration affect Worker-bound R2 access?

No. CORS rules only apply to direct HTTP access to the R2 bucket endpoint (r2.dev domain or custom domain). Worker bindings access R2 via the internal API and are not subject to CORS. You only need CORS when browsers access R2 directly.

How do I allow all origins temporarily during development?

Set AllowedOrigins to ["*"] and AllowedMethods to ["GET", "HEAD"] for read-only testing. For PUT and DELETE, list specific origins. Use the dashboard or Wrangler to update rules when moving to production.

Summary

R2 CORS configuration controls which web origins can access your bucket objects from browser applications. Rules are set per bucket via Wrangler, the S3 API, or the Cloudflare Dashboard. Each rule specifies allowed origins, methods, headers, and preflight cache duration. For production workloads, list specific origins instead of using the wildcard to maintain security while enabling cross-origin access.

This guide is brought to you by the developers of Cloudflare, Cloudflare R2, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro