diff --git a/.changeset/nasty-years-taste.md b/.changeset/nasty-years-taste.md new file mode 100644 index 0000000000..d15c725d32 --- /dev/null +++ b/.changeset/nasty-years-taste.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Improve validation and error messaging for R2 CORS configuration files to catch AWS S3-style formatting mistake. diff --git a/packages/wrangler/src/__tests__/r2/bucket.test.ts b/packages/wrangler/src/__tests__/r2/bucket.test.ts index aa2651106f..34d8827264 100644 --- a/packages/wrangler/src/__tests__/r2/bucket.test.ts +++ b/packages/wrangler/src/__tests__/r2/bucket.test.ts @@ -3550,6 +3550,48 @@ describe("r2", () => { }); }); describe("set", () => { + it("should reject AWS S3 format with CORSRules key", async () => { + const filePath = "cors-s3-format.json"; + const s3Config = { + CORSRules: [ + { + AllowedOrigins: ["*"], + AllowedMethods: ["GET"], + }, + ], + }; + + fs.writeFileSync(filePath, JSON.stringify(s3Config)); + + await expect( + runWrangler( + `r2 bucket cors set my-bucket --file ${filePath} --force` + ) + ).rejects.toThrowError( + /Wrangler detected an AWS S3 CORS configuration format/ + ); + }); + + it("should reject AWS S3 style PascalCase keys in rules", async () => { + const filePath = "cors-s3-keys.json"; + const s3StyleConfig = { + rules: [ + { + AllowedOrigins: ["*"], + AllowedMethods: ["GET"], + }, + ], + }; + + fs.writeFileSync(filePath, JSON.stringify(s3StyleConfig)); + + await expect( + runWrangler( + `r2 bucket cors set my-bucket --file ${filePath} --force` + ) + ).rejects.toThrowError(/Wrangler detected AWS S3 style keys/); + }); + it("should set CORS configuration from a JSON file", async () => { const bucketName = "my-bucket"; const filePath = "cors-configuration.json"; diff --git a/packages/wrangler/src/r2/cors.ts b/packages/wrangler/src/r2/cors.ts index 941d61e4b2..9dc024092a 100644 --- a/packages/wrangler/src/r2/cors.ts +++ b/packages/wrangler/src/r2/cors.ts @@ -100,17 +100,49 @@ export const r2BucketCORSSetCommand = createCommand({ const jsonFilePath = path.resolve(file); - const corsConfig = parseJSON(readFileSync(jsonFilePath), jsonFilePath) as { - rules: CORSRule[]; - }; + const corsConfig = parseJSON( + readFileSync(jsonFilePath), + jsonFilePath + ) as Record; - if (!corsConfig.rules || !Array.isArray(corsConfig.rules)) { + // Detect AWS S3 top-level format (CORSRules instead of rules) + if (corsConfig.CORSRules) { throw new UserError( - `The CORS configuration file must contain a 'rules' array as expected by the request body of the CORS API: ` + + "Wrangler detected an AWS S3 CORS configuration format.\n" + + "Cloudflare R2 expects a 'rules' array instead of 'CORSRules'.\n" + + "See: https://developers.cloudflare.com/r2/buckets/cors/#example" + ); + } + + // Validate existence of rules array + const rules = corsConfig.rules; + if (!rules || !Array.isArray(rules)) { + throw new UserError( + `The CORS configuration file must contain a 'rules' array as expected by the R2 API: ` + `https://developers.cloudflare.com/api/operations/r2-put-bucket-cors-policy` ); } + // Detect AWS S3 individual rule format (AllowedOrigins, AllowedMethods, AllowedHeaders) + const hasS3Keys = (rules as Record[]).some( + (rule) => + rule && + typeof rule === "object" && + !Array.isArray(rule) && + ("AllowedOrigins" in rule || + "AllowedMethods" in rule || + "AllowedHeaders" in rule) + ); + + if (hasS3Keys) { + throw new UserError( + "Wrangler detected AWS S3 style keys (e.g. 'AllowedOrigins').\n" + + "Cloudflare R2 requires lowercase keys nested inside an 'allowed' object.\n" + + 'Example: { "allowed": { "origins": ["*"], "methods": ["GET"] } }\n' + + "See: https://developers.cloudflare.com/r2/buckets/cors/#example" + ); + } + if (!force) { const confirmedRemoval = await confirm( `Are you sure you want to overwrite the existing CORS configuration for bucket '${bucket}'?` @@ -122,13 +154,13 @@ export const r2BucketCORSSetCommand = createCommand({ } logger.log( - `Setting CORS configuration (${corsConfig.rules.length} rules) for bucket '${bucket}'...` + `Setting CORS configuration (${rules.length} rules) for bucket '${bucket}'...` ); await putCORSPolicy( config, accountId, bucket, - corsConfig.rules, + rules as CORSRule[], jurisdiction ); logger.log(`✨ Set CORS configuration for bucket '${bucket}'.`);