Workers R2 -- CORS Configuration
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
- What HTTP method does a browser send as a CORS preflight request?
- Which field in a CORS rule specifies which websites can access the bucket?
- Why should you avoid using
*for AllowedOrigins in production?
FAQ
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