跳到主要內容
AnyStorage
免費下載

Cloudflare R2

R2 CORS 錯誤:沒有回傳跨來源允許標頭

瀏覽器存取 Cloudflare R2 出現 CORS,是儲存桶政策的問題,不是程式碼的問題。各欄位分別管什麼、三種下發方式,以及 r2.dev 的陷阱。

R2 為什麼不回傳 Access-Control-Allow-Origin、CORS 政策每個欄位控制什麼,以及用控制台、Wrangler 或 S3 API 下發政策的做法。

r2 cors 錯誤,r2 cors 政策,Cloudflare R2 Access-Control-Allow-Origin

主控台裡的原文,以及它不是什麼

text

Access to fetch at 'https://<ACCOUNT_ID>.r2.cloudflarestorage.com/bucket/key'
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.

你的 fetch 沒寫錯,預簽章 URL 大概也有效,同一個物件用 curl 可以順利下載。是瀏覽器不肯把回應交給你的 JavaScript,因為 R2 沒說「可以交」。

Cloudflare 的 R2 CORS 文件裡有兩個事實框住整件事。第一,政策掛在儲存桶上,不在程式碼裡。第二,「Only a cross-origin request will include CORS response headers」——R2 依 Origin 標頭判斷是否跨來源。所以把同一個 URL 貼到網址列可以開,而這件事什麼也證明不了。

失敗的樣子與對應欄位
主控台症狀原因要改的欄位
完全沒有 Access-Control-Allow-Origin儲存桶上沒有 CORS 政策AllowedOrigins
GET 正常,只有 PUT 失敗方法未被允許AllowedMethods
"Request header field content-type is not allowed"標頭沒被列出AllowedHeaders
上傳成功但 response.headers.get('etag')null標頭沒有公開ExposeHeaders
https://example.com 可用,https://www.example.com 不行來源必須完全相符AllowedOrigins
403 且完全沒有 CORS 標頭預簽章 URL 過期重新產生 URL

1. 把政策寫明確

R2 接受 S3 風格的 CORS JSON,五個欄位:AllowedOriginsAllowedMethodsAllowedHeadersExposeHeadersMaxAgeSeconds。Cloudflare 自己的瀏覽器上傳範例就是值得照抄的形狀:

json

[
  {
    "AllowedOrigins": ["https://example.com"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["Content-Type"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

注意這個範例沒有做的事:它沒有在 AllowedHeaders"*",而是寫出用戶端真正會送的標頭名稱。這個錯誤背後的幾條 Cloudflare Community 討論幾乎都是同一個故事:以為 "*" 會像在 AWS 上一樣有效,預檢卻始終不過,換成字面標頭名之後一行就修好。寫清楚的成本是一行,消掉的是一整類懷疑。

通用字元的規則從瀏覽器端也值得知道。MDN 整理的 CORS 規範指出,回應帶憑證的請求時,伺服器對 Access-Control-Allow-Origin「must not specify the * wildcard ... but must instead specify an explicit origin」,同樣的限制也適用於 Access-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Expose-Headers

2. 下發政策:控制台、Wrangler、S3 API

控制台:R2 object storage → 目標儲存桶 → Settings,在 CORS Policy 下選 Add CORS policy,貼上 JSON。

能留在儲存庫裡的是 Wrangler 版本:

sh

npx wrangler r2 bucket cors set my-bucket --file cors.json
npx wrangler r2 bucket cors list my-bucket

R2 也說 S3 相容 API,所以走 CLI 同樣可行。AWS CLI 的區域填 auto——Cloudflare 的 CLI 範例註明它只是 SDK 要求,R2 並不使用:

sh

aws s3api put-bucket-cors \
  --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
  --bucket my-bucket \
  --cors-configuration file://cors.json

aws s3api get-bucket-cors \
  --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
  --bucket my-bucket

重測之前等幾秒。R2 文件提醒政策變更需要時間傳播,而剛剛快取了一次失敗預檢的瀏覽器,在 MaxAgeSeconds 到期前不會再問一次。Cloudflare 也說明瀏覽器可能把這個快取時間壓到兩小時左右。

3. 瀏覽器實際在問什麼

失敗的往往不是你的 PUT,而是預檢。MDN 的說法是:「for 'preflighted' requests the browser first sends an HTTP request using the OPTIONS method to the resource on the other origin, in order to determine if the actual request is safe to send.」這個 OPTIONS 帶著 Access-Control-Request-MethodAccess-Control-Request-Headers,政策必須同時滿足兩者。

不用瀏覽器也能重現,猜測立刻變成兩秒的確認:

sh

curl -i -X OPTIONS \
  -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: content-type" \
  "https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/test.png"

政策正確時,回應會帶上與請求相符的 Access-Control-Allow-Origin-Methods-Headers。其他結果都是政策問題,不是框架問題。

簡單請求根本不會觸發預檢:MDN 列出的是 GETHEADPOST 加上安全清單標頭——AcceptAccept-LanguageContent-Language、限定三種 MIME 的 Content-Type,以及 Range。一旦加上 x-amz-* 或 JSON 的 content type,就又回到預檢範圍。

4. 來源是字串,不是樣式

AllowedOrigins 的每一項必須與瀏覽器的 Origin 完全相符:協定、主機、埠都算,不能寫路徑。http://localhost:3000http://localhost:5173 是兩項,https://example.com 不涵蓋 https://www.example.com,結尾多一個斜線就不相符。

5. r2.dev 與自訂網域

如果你用公開開發 URL 測試,先讀這段。Cloudflare 明確寫著「Public access through r2.dev subdomains is rate-limited and should only be used for development purposes」,並警告不要把 CNAME 指向 r2.dev 子網域,因為那是「an unsupported access path」。

自訂網域的行為更順,而且有文件依據:掛了 CORS 政策的儲存桶接上自訂網域後,會對跨來源請求自動回傳 CORS 回應標頭。由此有兩個後果。自訂網域在 Cloudflare 快取之後,所以改完政策可能需要清快取,舊的預檢回應才會停止被送出。另外預簽章 URL 是對產生時的主機簽的,為帳戶端點產生的 URL 在自訂網域上不會通。

6. 連 CORS 標頭都沒有的上傳失敗

過期看起來像 CORS 錯誤,但不是。R2 文件指出過期的預簽章 URL 會回傳不含 CORS 標頭的 403 ExpiredRequest,於是瀏覽器只報「缺少標頭」,把真正原因藏起來。動政策之前先看 X-Amz-Expires 與時鐘。如果上傳成功卻讀不到 ETag,那是 ExposeHeaders,改一個陣列就好。

有時候根本不必經過瀏覽器

CORS 是保護瀏覽器的機制,所以只對瀏覽器生效。桌面用戶端自己簽章、不送 Origin 標頭,任何政策都攔不住它——這就是為什麼一個在 Web 應用裡不能用的儲存桶,同一分鐘在應用程式裡瀏覽正常。

AnyStorage(v0.2.24;macOS 11+、Windows 10+、Ubuntu 20.04+)把 R2 當作 S3 相容端點:帳戶端點 URL、R2 的金鑰對,path-style 定址與自訂區域都是顯式設定。它透過內建的本機 WebDAV 服務(http://127.0.0.1:3211)掛載成磁碟機,不需要 FUSE、WinFsp 或 macFUSE,免費版可用兩個連線。只要是搬檔案,完全不涉及 CORS 政策;要做 Web 應用,上面那份政策還是得寫。

能不能乾脆全部放開?

AllowedOrigins 設成 ["*"] 並列出用到的方法,對一個放公開圖片的儲存桶說得過去。但不要碰 AllowedHeaders"*":Cloudflare 自己的範例寫的是標頭名稱,社群回報中上傳卡住的也正是這裡。

昨天還能用,今天為什麼不行?

常見答案三個:來源變了(預覽部署會拿到新主機名)、儲存桶重建後政策不見了、預檢回應是改動之前快取的。用上面的 curl -X OPTIONS 看一眼 R2 現在到底回什麼。

伺服器端上傳需要設定 CORS 嗎?

不需要。Worker、Node 程序、CI 工作都不受 CORS 約束,受約束的只有瀏覽器 JavaScript 發出的請求。

政策會影響 Worker 裡的 R2 綁定嗎?

不會。儲存桶綁定走的是 Cloudflare 內部 API,不是 S3 端點,CORS 不在鏈路上。政策只對瀏覽器直接存取儲存桶的請求有意義。

下一步