Problem with settings CORS policies on R2

Hi @siddhant1!

I’m seeing the CORS errors when I try to upload to R2 using a presigned post request. Using using AWS S3 everything works, but when I switch over to R2 I start getting CORS errors.

Here’s what I’m doing:

My server side code is using the @aws-sdk/s3-presigned-post library to generate params needed to post a file upload to R2.

import { S3Client } from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';

let accountId = "XXX";
let accessKeyId = "XXX";
let secretAccessKey = "XXX";

let s3Client = new S3Client({
  credentials: {
  region: "auto",
  endpoint: `https://${accountId}`

let presignedPost = await createPresignedPost(s3Client, {
  Bucket: bucket,
  Key: key,
  Fields: {
    'Content-Type': "image/jpeg",
  Expires: 60 * 60, // 1 hour

Next, the browser is using the data in presignedPost to send the following XHR request to R2:

let file = "..." // the selected jpeg file from the file <input>
let formData = new FormData();

Object.entries({ ...presignedPost.fields, file }).forEach(
  ([field, value]) => {
    formData.append(field, value);

let xhr = new XMLHttpRequest();'POST', presignedPost.url, true);

This sends a POST request to the URL: with the following as form data:

Content-Type: image/jpeg
bucket: XXX
X-Amz-Algorithm: AWS4-HMAC-SHA256
X-Amz-Credential: XXX
X-Amz-Date: 20221201T030934Z
key: XXX
Policy: XXX
X-Amz-Signature: XXX
file: (binary)

And I receive the following error from R2.

Access to XMLHttpRequest at '' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

I have confirmed that my R2 bucket has been setup to allows CORS requests using:

await s3Client.send(
  new PutBucketCorsCommand({
    Bucket: "XXX",
    CORSConfiguration: {
      CORSRules: new Array({
        AllowedHeaders: ["*"],
        // Also tried using:
        // AllowedHeaders: ["Content-Type"],
        AllowedMethods: ["GET", "PUT", "POST", "HEAD"],
        AllowedOrigins: ["*"],
        ExposeHeaders: [],
        MaxAgeSeconds: 3000

I’ve commented out my api keys, account id, bucket name, and other sensitive information with XXX. Just to confirm, this all works when I use S3, but when I switch to R2 I start getting errors.

Hopefully this helps, but please let me know if there’s any more information I should provide.

FWIW I’ve also been trying to get pre-signed PUT urls for R2 with the sig generated in a Worker using aws4fetch working - alas to no success. I’ve followed instructions on
My hunch is that the issue is with aws4fetch, as I also tried this with an S3 bucket but had issues with the sig.

It would be great if there was an end-to-end example of using R2 with pre-signed PUT urls generated in a Worker.

For now I’ve had to switch my project from Workers to Node :frowning:

I’ve also had trouble. I’m trying to upload from localhost. I’ve set a custom domain for the r2 bucket if that matters. I’ve tried use wildcards and explicitly setting http://localhost:4000 as an origin. I’ve tried using an https://#{account_id} url as well as the custom domain url to send the put request to. If I do the former, I get a 400 error. The latter gives a 413. Both say it’s access control problems @siddhant1

const data = await s3.send(new PutBucketCorsCommand({
      Bucket: "assets",
      CORSConfiguration: {
        CORSRules: new Array({
          // AllowedOrigins: ["*"],
          AllowedOrigins: ["http://localhost:4000"],
          AllowedHeaders: ["*"],
          AllowedMethods: ["GET", "PUT", "POST", "HEAD", "DELETE"],
          ExposeHeaders: ["*"],
          MaxAgeSeconds: 3000

You’ve generated a GetObjectCommand and you’re trying to send a PUT request to it - GET !== PUT. This isn’t a CORS issue.

1 Like

You’re right. I’m so sorry, that should’ve been obvious. Thanks for pointing it out!

After several weeks struggling with R2 + CORS (with no success) I managed to get that “access-control-allow-origin=*” on the media files uploaded to R2 in this way:

  1. Connect the R2 bucket to a custom domain (i.e. hosted on Cloudflare
  2. Go to the Domain Dashboard and go to Rules > Transform Rules > Modify Response Header
  3. Create a New Rule: if the Hostname equals to then add the Header “access-control-allow-origin=*”
  4. Deploy

Immediately all assets hosted on began showing that magic header : )



Thank you. This has worked.

Thanks, it helped but it is not solution in general as it not able to allow only specific domains (it not works)
I prepared demo page and going to ping devs in Discord.

Same error here; impossible to make CORS work. I tried all above options without success.
Had to switch to DigitalOcean Space which works

FWIW there are now docs (Configure CORS · Cloudflare R2 docs) and a UI to configure CORS, if people are having trouble doing so via the S3 API

@siddhant1 I’ve had to resort to the fix by @giorgio.scavuzzo to be able to retrieve images from R2 as array buffers using fetch. I have configured the allowed domains using the UI but without the aforementioned fix, it still wouldn’t work.

Could you kindly look into this?

I am unable to edit my post above so I’m posting the fix here.
The reason I was facing CORS errors was because was loading the same image separately using an img tag. The browser was caching that request which did not have the required CORS headers, resulting in an error. I fixed it by simply adding crossorigin=“anonymous” to the img tag so that the cached image request has the required CORS headers.

i tried to apply CORS from R2 dashboard, with custom domain settings, And I added the file to the website which I have set in AllowedOrigins"(in this case I tried the .mp4 file).And I get error’ 400 response in browser console.

Is there any solution?

I’m with the same problem.

I’m using preSignedUrl for Upload file. (PutCommand)

When I Tried to upload from http://localhost:4200 the OPTIONS request return 204 (OK) but the PUT request returns 400 with No 'Access-Control-Allow-Origin ERROR.

The response on OPTIONS request show the CORS headers:

But the PUT request no:

    "AllowedOrigins": [
    "AllowedMethods": [
    "AllowedHeaders": [
    "MaxAgeSeconds": 2

What is the body of the 400 response?

The body is the binary of file (csv) to upload.

The error on console is: has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

I Fixed here now!

changed the AllowedHeaders to:

“Content-Type” instead of *

My issue is exactly like @robson’s: The OPTIONS (preflight) request gets a 204 response with Access-Control-Allow-Origin: *, which is what I had set in the CORS policy. So far, so good. However, the subsequent POST request gets back a 400 and doesn’t have any CORS headers. (Also, explicitly enumerating allowed headers like the above comment suggests, doesn’t help).

:warning: We are using POST, not PUT, because some options that we need (such as dynamic conditions on the upload path and content type, such as ["starts-with", "$key", "user/user1/"]) are only available with POST requests (AWS docs and

Could it be that the R2 doesn’t implement this (POST) endpoint at all (yet)?

I see you added a lot of docs since I last looked into this, and also a CORS policy editor (thanks for that, it saves a lot of messing around with cURL and Postman :sweat_smile:). I’ll take another look at the docs to see if there’s anything you added there that may provide some hints.

Please, @mtarnovan, do not post twice.

Sorry, the spam filter kicked in and I accidentally double posted. I deleted the 2nd post now.

1 Like