R2 seems to be ignoring the CORS policy

I have a bucket that i'm trying to upload to via signed URLs generated using aws-sdk Node package The signed URL always fails PUT request with Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Here's my CORS policy, which should work? What am I missing?
[
{
"AllowedOrigins": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"HEAD"
],
"AllowedHeaders": [
"range"
],
"ExposeHeaders": [
"Content-Type",
"Access-Control-Allow-Origin",
"ETag"
]
}
]
[
{
"AllowedOrigins": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"HEAD"
],
"AllowedHeaders": [
"range"
],
"ExposeHeaders": [
"Content-Type",
"Access-Control-Allow-Origin",
"ETag"
]
}
]
6 Replies
Hard@Work
Hard@Work•3d ago
Cloudflare Docs
Troubleshooting · Cloudflare R2 docs
If you are encountering a CORS error despite setting up everything correctly, you may follow this troubleshooting guide to help you.
max
maxOP•3d ago
i've seen this + multiple forums post, no dice
Hard@Work
Hard@Work•3d ago
Do you have a repro somewhere I can test with?
max
maxOP•3d ago
not at the moment, it's rather painful to extract from the main project 🫠
Hard@Work
Hard@Work•3d ago
Can you copy just the part that does the signature? I can try to start from there at least
max
maxOP•3d ago
oh i think i fixed it, changed AllowedHeaders to
"AllowedHeaders": [
"range",
"Content-Type"
],
"AllowedHeaders": [
"range",
"Content-Type"
],
and now it's a 403 instead of preflight error, so i can at least debug further 😄 nvm still same issue one sec
import { authenticateToken } from "@/api/auth"
import { Integration } from "@/api/models/integration.model"
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import type { NextApiRequest, NextApiResponse } from "next"
import slugify from "slugify"
import { v4 as uuidv4 } from "uuid"

const s3Client = new S3Client({
region: "auto",
endpoint: R2_ENDPOINT,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID!,
secretAccessKey: R2_SECRET_ACCESS_KEY!,
},
})

const sanitizeFilename = (filename: string): string => {
const nameWithoutExtension = filename.substring(0, filename.lastIndexOf(".")) || filename
const extension = filename.substring(filename.lastIndexOf("."))

return slugify(nameWithoutExtension, { lower: true, strict: true }) + extension
}

export default async function handler(req: AuthenticatedRequest, res: NextApiResponse) {
const { integrationId, filename, contentType, type } = req.body

try {
const sanitized = sanitizeFilename(filename)
const uniqueId = uuidv4().split("-")[0]
const key = `integrations/${type}/${uniqueId}-${sanitized}`

const command = new PutObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: key,
ContentType: contentType,
})

const expiresIn = 300 // 5min
const uploadUrl = await getSignedUrl(s3Client as any, command, { expiresIn })

res.status(200).json({
uploadUrl: `${uploadUrl}&cors=1`, // some post somewhere said this might help - it didn't
key: key,
})
} catch (error: any) {
console.error("Error generating signed URL:", error)
res.status(500).json({ error: "Failed to generate upload URL.", details: error.message })
}
}
import { authenticateToken } from "@/api/auth"
import { Integration } from "@/api/models/integration.model"
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import type { NextApiRequest, NextApiResponse } from "next"
import slugify from "slugify"
import { v4 as uuidv4 } from "uuid"

const s3Client = new S3Client({
region: "auto",
endpoint: R2_ENDPOINT,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID!,
secretAccessKey: R2_SECRET_ACCESS_KEY!,
},
})

const sanitizeFilename = (filename: string): string => {
const nameWithoutExtension = filename.substring(0, filename.lastIndexOf(".")) || filename
const extension = filename.substring(filename.lastIndexOf("."))

return slugify(nameWithoutExtension, { lower: true, strict: true }) + extension
}

export default async function handler(req: AuthenticatedRequest, res: NextApiResponse) {
const { integrationId, filename, contentType, type } = req.body

try {
const sanitized = sanitizeFilename(filename)
const uniqueId = uuidv4().split("-")[0]
const key = `integrations/${type}/${uniqueId}-${sanitized}`

const command = new PutObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: key,
ContentType: contentType,
})

const expiresIn = 300 // 5min
const uploadUrl = await getSignedUrl(s3Client as any, command, { expiresIn })

res.status(200).json({
uploadUrl: `${uploadUrl}&cors=1`, // some post somewhere said this might help - it didn't
key: key,
})
} catch (error: any) {
console.error("Error generating signed URL:", error)
res.status(500).json({ error: "Failed to generate upload URL.", details: error.message })
}
}
Tried rotating the keys and every variation of the policy JSON i can think of, still getting the No 'Access-Control-Allow-Origin' header is present on the requested resource. Tried console.log(await s3Client.send(new ListBucketsCommand({}))) from the examples, got an error Changed the api token from object to admin Managed to list Changed it back Managed to upload one time Repeated the upload - 403 The good news is cors seems solved, the bad news is the inconsistent 403

Did you find this page helpful?