How to Implement CORS Policies for R2 Storage Buckets in Next.js Cloudflare Workers
To implement CORS policies for Cloudflare R2 storage buckets, you must manually inject Access-Control-Allow-Origin and related headers into the Worker response that serves the object, since R2 does not provide a native CORS configuration interface like AWS S3.
Unlike AWS S3, Cloudflare R2 does not expose a UI or API for bucket-level CORS configuration. In the ifindev/fullstack-next-cloudflare repository, you implement CORS policies for R2 storage buckets by attaching headers at the edge Worker level before streaming objects to the browser. This approach leverages the next_cf_app_bucket binding declared in worker-configuration.d.ts and the helper functions in src/lib/r2.ts to serve files with strict security controls.
Understanding the R2 CORS Architecture
Cloudflare R2 stores objects without evaluating CORS rules during retrieval. To implement CORS policies for R2 storage buckets, you intercept the request in your Next.js API route or Worker, fetch the object via env.next_cf_app_bucket.get(), and construct a new Response that includes the necessary CORS headers. This grants you full programmatic control over which origins can access your stored assets.
Creating a Reusable CORS Utility
Reusable logic prevents header inconsistencies across endpoints. Create src/lib/cors.ts to generate standardized headers for every R2-serving route.
// src/lib/cors.ts
export function corsHeaders(origin = "*"): HeadersInit {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400", // 24 hours
};
}
This helper returns the headers required for pre-flight and actual requests, allowing you to whitelist specific origins or permit global access.
Serving R2 Objects with CORS Headers
The repository’s R2 interaction layer resides in src/lib/r2.ts, which exports getFromR2 for object retrieval. To implement CORS policies for R2 storage buckets in a public API, create a dynamic route at src/app/api/files/[...key]/route.ts that wraps the R2 object in a CORS-enabled response.
Handling GET Requests
// src/app/api/files/[...key]/route.ts
import { getFromR2 } from "@/lib/r2";
import { corsHeaders } from "@/lib/cors";
export async function GET(request: Request, { params }: { params: { key: string[] } }) {
const key = params.key.join("/");
const object = await getFromR2(key);
if (!object) {
return new Response("Not found", { status: 404 });
}
const headers = new Headers({
"Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream",
...corsHeaders(),
});
return new Response(object.body, { status: 200, headers });
}
The getFromR2 function retrieves the R2ObjectBody from the binding defined in worker-configuration.d.ts, and the spread operator injects the CORS headers into the final response.
Handling Pre-flight OPTIONS Requests
Browsers send an OPTIONS request before cross-origin GET calls. You must explicitly handle this to implement CORS policies for R2 storage buckets correctly.
// src/app/api/files/[...key]/route.ts (continued)
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: corsHeaders(),
});
}
Returning a 204 No Content response with the same headers satisfies the CORS pre-flight check without streaming unnecessary data.
Applying CORS to Existing Upload Workflows
If your application already uses uploadToR2 from src/lib/r2.ts to store files, you can extend those modules to serve files with restricted origins. Import corsHeaders into any existing route and apply it when returning R2 objects:
return new Response(object.body, {
status: 200,
headers: {
"Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream",
...corsHeaders("https://my-frontend.example.com"),
},
});
This restricts access to a specific frontend domain while maintaining the streaming performance of R2.
Summary
- Cloudflare R2 does not provide native CORS configuration; you must implement CORS policies for R2 storage buckets at the Worker level using response headers.
- Create a centralized
corsHeadersutility insrc/lib/cors.tsto ensure consistent security policies across endpoints. - Use
getFromR2fromsrc/lib/r2.tsto fetch objects and wrap them in aResponsewith CORS headers. - Always export an
OPTIONShandler that returns a 204 response with CORS headers to support browser pre-flight requests. - Reference the
next_cf_app_bucketbinding declared inworker-configuration.d.tswhen accessing bucket resources.
Frequently Asked Questions
Does Cloudflare R2 support bucket-level CORS configuration like AWS S3?
No. According to the source code analysis, R2 does not expose a native CORS configuration UI or API. To implement CORS policies for R2 storage buckets, you must attach Access-Control-Allow-Origin and related headers in the Worker or API route that serves the file, as demonstrated in the src/app/api/files/[...key]/route.ts implementation.
Where is the R2 bucket binding defined in the fullstack-next-cloudflare project?
The bucket binding is declared in worker-configuration.d.ts as next_cf_app_bucket. This binding is used throughout the application, particularly in src/lib/r2.ts where the getFromR2 and uploadToR2 functions interact with the R2 storage bucket.
How do I restrict CORS to a specific origin instead of allowing all domains?
Pass a specific origin string to the corsHeaders function instead of the default "*" wildcard. For example, use corsHeaders("https://my-frontend.example.com") when constructing the response headers in your API route. This ensures only requests from that specific origin can access the R2-stored assets.
Why is an OPTIONS handler required for CORS implementation?
Browsers perform a pre-flight check using an OPTIONS request before executing cross-origin GET or POST requests. When you implement CORS policies for R2 storage buckets, you must handle this by returning a 204 No Content response with the appropriate CORS headers, as shown in the route handler example, otherwise the browser blocks the subsequent request.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →