Admin- Debugging CORS Issue When Serving Images from R2

Recently, I encountered a particularly tricky CORS-related bug while building the image intake + cropping pipeline for my SaaS project, NestButter. The setup seemed simple: upload to R2 using a presigned URL, then directly load the image from R2 via its public URL to perform further operations like cropping.

But something strange happened...


📝 Background: My Upload and Access Pipeline

I was using Cloudflare R2 to store images. My upload flow looked like this:

  1. Upload

    • On the server, I generated a PUT presigned URL via R2 SDK

    • On the client, I used that URL to PUT the image directly from the browser

  2. Access

    • After upload, I would form the direct image URL like this:

      const url = `https://user-imgs.nestbutter.com/store/${sha}.webp`;
      
    • I would then load this image via <img> or inside a cropping modal via new Image()

Seems reasonable, right?


❌ The Problem: src is Correct, But Image Fails to Load in Modal

After uploading, I immediately attempted to open the image in a cropping modal. The modal uses this logic:

const im = new Image();
im.crossOrigin = "anonymous";
im.src = url;

Surprisingly:

  • src was correct (I logged it)

  • img in the UI also showed the image fine

  • But when Image() tried to load it in code, I got:

Access to image at 'https://user-imgs.nestbutter.com/store/xxx.webp'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

So the modal failed, defaulted to asking the user to pick a file again.


🕵️ Step-by-Step Debugging

1. Check if src is really missing

I logged src in both the modal trigger and inside Image(). Confirmed: it's correct.

2. Verify CORS Policy in R2 Console

I had this at first:

Allowed Origins: http://localhost:3000, https://nest-butter.vercel.app

Wrong! R2 does not support comma-separated origins.

3. Fixed to split into two rows:

| http://localhost:3000            | GET, HEAD, PUT | * |
| https://nest-butter.vercel.app  | GET, HEAD, PUT | * |

Still didn't work.

4. Inspect image headers

I opened the image in browser and checked the Response Headers. To my surprise:

  • No Access-Control-Allow-Origin

  • Cache status: Cf-Cache-Status: HIT

Then it clicked.


⚡ Root Cause: Cloudflare Cached a Version Without CORS

Even though I updated the CORS config on the bucket, Cloudflare had already cached an earlier version of the response without those headers.

Hence:

  • The header was not present

  • The browser blocked access

  • Image() onerror triggered


🔧 Solution: Use a Cloudflare Worker to Add CORS

Instead of depending on R2's CORS config (which can be silently bypassed due to CDN caching), I deployed a simple Cloudflare Worker as a proxy layer:

Worker Code:

export default {
  async fetch(req) {
    const url = new URL(req.url);
    const key = url.pathname.replace("/proxy/", "store/");
    const r2Resp = await fetch(`https://user-imgs.nestbutter.com/${key}`);

    const headers = new Headers(r2Resp.headers);
    headers.set("Access-Control-Allow-Origin", "*");
    headers.set("Access-Control-Allow-Methods", "GET, HEAD");
    headers.set("Access-Control-Allow-Headers", "*");
    headers.set("Cache-Control", "public, max-age=31536000");

    return new Response(r2Resp.body, {
      status: r2Resp.status,
      headers,
    });
  }
};

CDN URL before:

https://user-imgs.nestbutter.com/store/abc123.webp

After (Proxy URL):

https://img.nestbutter.com/proxy/abc123.webp

In my app, I simply added:

function cdnProxy(storePath: string) {
  return storePath.replace(
    "https://user-imgs.nestbutter.com/store/",
    "https://img.nestbutter.com/proxy/"
  );
}

✅ Final Result

  • Uploaded images instantly available for <img> and <Image>

  • No more CORS issues

  • Worker gives me full control over headers + future CDN behaviors

  • Don't have to rely on R2's flaky built-in CORS


☑️ Takeaways

  • Don't trust Cloudflare R2's CORS config if you're also using Cloudflare caching

  • Never test CORS via address-bar opening; always test via JS fetch / Image

  • Use Worker proxy for full control: clean, flexible, and safe

  • CDN caching can silently cause stale CORS headers to persist


Hope this helps others fighting with presigned URLs + CORS + R2 + Cloudflare cache madness. Let me know if you'd like a full wrangler deployment config!

Stay sharp ⚡

posted @ 2025-11-15 14:28  PEAR2020  阅读(6)  评论(0)    收藏  举报