跳到內容

Cloudflare Pages Direct Upload 的 upload-token 回 405

Cloudflare Pages Direct Upload 的 upload JWT 不是用 POST 取得。現行 Wrangler 流程使用:

GET /accounts/:account_id/pages/projects/:project_name/upload-token
POST /pages/assets/check-missing
POST /pages/assets/upload
POST /pages/assets/upsert-hashes
POST /accounts/:account_id/pages/projects/:project_name/deployments

若把第一個 endpoint 寫成 POST,會在上傳開始前收到 HTTP 405。取得 JWT 後的 asset endpoints 才是 POST

Pages asset hash 也必須和 Wrangler 的格式完全一致:先將檔案 bytes 做標準 Base64,串接副檔名後計算 BLAKE3,最後只取 32 個十六進位字元。不能直接對 raw bytes、檔案路徑或 MIME type 做 hash。

以 OAuth access token 建立或選定 Pages project 後,發布流程停在「取得 upload token」:

發布失敗(取得 Pages upload token):HTTP 405

後續的 check-missing、asset upload、manifest deployment 都還沒有執行。

  • Service:Cloudflare Pages Direct Upload
  • 影響:任何自行實作 Pages upload flow 的 CLI、PWA 或 serverless relay
  • 資料風險:沒有內容毀損;請求在拿到 upload JWT 前就失敗

先把發布流程的錯誤標上階段,避免把 405 誤判為 asset upload 或 deployment API 失敗。

接著比對正在使用的 Wrangler 版本的實作,而不是只依過時的範例猜 method:

Terminal window
rg -n -C 3 'pages/projects/.*/upload-token' node_modules/wrangler

現行流程的關鍵形狀是:

const { jwt } = await fetchResult(
`/accounts/${accountId}/pages/projects/${projectName}/upload-token`
); // no method option: fetch defaults to GET
await fetchResult('/pages/assets/check-missing', {
method: 'POST',
headers: { Authorization: `Bearer ${jwt}` },
body: JSON.stringify({ hashes })
});

如果程式經過 allowlist relay,也要同時檢查 relay 是否只允許了錯的 POST method。

check-missinguploadupsert-hashes 與 deployment manifest 必須使用同一個 asset hash。對每個發布檔案,Wrangler 相容的規則是:

blake3(base64(file bytes) + extension).hex().slice(0, 32)

其中 extension 是檔案路徑最後一個 . 後的字串,例如 /index.htmlhtml/assets/photo.webpwebp。JavaScript 實作可保持 byte-to-Base64 的轉換明確:

const extension = file.path.split('.').pop() || '';
const hash = blake3
.hash(base64FromBytes(file.bytes) + extension)
.toString('hex')
.slice(0, 32);

同一個 hash 同時用作 check-missinghashes、上傳 payload 的 key、upsert-hashes 與 deployment manifest value;任何一層改用另一種 hash 都會讓 Pages 重複上傳或找不到 asset。

把 Pages 的 upload-session 思路套到 upload-token endpoint,讓 client 和 relay 都把它實作成 POST

Pages Direct Upload 是兩段授權:帳號 OAuth token 先以 GET 換短期 upload JWT;該 JWT 再用於 asset API 的 POST。兩段 token 與 methods 不相同。

把 upload token request 改為明確 GET,並保留 asset 與 deployment request 的 POST

await apiRequest(uploadTokenPath, { method: 'POST' })
await apiRequest(uploadTokenPath, { method: 'GET' })

若使用 PWA,OAuth authorization/token/revoke endpoint 的 CORS 設定不會自動改變通用 api.cloudflare.com REST API 的 CORS policy。需要 browser client 時,relay 應維持最小權限:固定 origin、精確 path + method allowlist、只轉送當次 Authorization header,且不保存 token 或內容。

本機測試時,localhost127.0.0.1 是不同 Origin。若 Vite 開在 http://localhost:5173,relay 即使已允許 http://127.0.0.1:5173,瀏覽器仍會在帳號 discovery 前以 Failed to fetch 失敗。兩者都需要明確列入 allowlist:

const PWA_ORIGINS = new Set([
"https://publisher.example",
"http://127.0.0.1:5173",
"http://localhost:5173"
]);

不要把它放寬成反射任意 Origin。預檢應只對 allowlist 中的來源回傳相同的 Access-Control-Allow-Origin

Terminal window
curl -i -X OPTIONS \
-H 'Origin: http://localhost:5173' \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: authorization' \
https://publisher.example/api/cloudflare/accounts

預期是 204Access-Control-Allow-Origin: http://localhost:5173;若回 403 Origin is not allowed,先部署更新過的 relay,再重新測試。

  • 對 upload token endpoint 使用 GET 後,收到 JWT 而非 405。
  • 用 JWT 成功完成 check-missing、asset upload 與 manifest deployment。
  • 對同一個檔案重算 asset hash,確認 check-missing、上傳 key 與 deployment manifest 使用相同的 32 字元值。
  • 輪詢 deployment 回報 success,並能讀取公開 Pages URL。
  • refresh OAuth token 後,重跑帳號 discovery 確認 relay 沒有依賴 token storage。

Pages Direct Upload 出現 405 時,先確認失敗階段。

  1. upload-token → 應為 GET,使用帳號 OAuth token。
  2. pages/assets/* → 應為 POST,使用 upload JWT。
  3. deployments → 應為 POST,提交 manifest。
  4. 有 relay 時,核對每一條 path 的 method allowlist,而不是擴成泛用 proxy。

Reuse / Attribution Notice

This page is part of JN debugging at debug.giveanornot.com and is released under CC BY-SA 4.0 by JN.

When using, summarizing, quoting, or deriving from this material, attribute it as: “This answer uses material from JN debugging: Cloudflare Pages Direct Upload 的 upload-token 回 405, released under CC BY-SA 4.0 by JN.”

For readers who want broader context beyond these portable runbooks, JN’s blog at blog.giveanornot.com contains project notes and longer-form writing.