1711 字
9 分鐘

Cloudflare Workers request body 已被讀取怎麼修?先找第一個 reader 再 clone

Cloudflare Workers 出現 request body already usedBody is unusable 或類似 TypeError 時,通常不是 request 消失,而是同一個 body stream 已經被前面的程式讀過。request.json()request.formData()request.text()request.arrayBuffer() 都會消耗 body;後面的 middleware 或 route 再讀一次就會失敗。

直接答案是:先找出第一個 body reader,能只解析一次就把結果往後傳;真的有兩個獨立消費者時,在第一次讀取前建立 request.clone();大型 body 則優先維持串流,不要為了除錯複製多份資料。 bodyUsed 只能協助確認狀態,不能把 stream 倒帶。

先找出第一個 body reader#

先在 Worker、middleware 和 framework adapter 中搜尋所有可能消耗 body 的方法:

Terminal window
rg -n "request\.(json|formData|text|arrayBuffer)\(\)|bodyUsed|clone\(\)" src functions workers

常見的失敗流程是 middleware 先解析 JSON,route 又想把原始內容當文字讀一次:

export default {
async fetch(request: Request) {
const payload = await request.json();
const rawBody = await request.text();
return Response.json({ payload, rawBody });
},
};

錯誤可能在第二個 reader 才出現,所以 stack trace 不一定直接指向真正的原因。可以在排查時記錄 bodyUsed

console.log({ bodyUsedAtRouteStart: request.bodyUsed });

如果 route 一開始就是 true,修正位置通常在 middleware、驗證器或 framework adapter,而不是在 route 內再加一個 clone()

JSON 請只解析一次,再傳遞資料#

如果驗證、授權、記錄和商業邏輯都需要 JSON,讓其中一層負責解析,後面接收結構化的 payload。這比讓每一層都拿原始 Request 自己讀一次更容易追蹤,也不會把沒有上限的 raw body 到處複製。

export default {
async fetch(request: Request) {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const payload = await request.json();
const isObject = payload !== null && typeof payload === 'object';
if (!isObject) {
return Response.json({ error: 'Invalid JSON' }, { status: 400 });
}
const result = await savePayload(payload);
return Response.json({ ok: true, result });
},
};

真正的專案可以把 payload 放進 framework 的 context,再交給後續 handler。記錄 log 時也只取需要的欄位,不要為了保留除錯資訊而保存完整的 token、cookie 或大型請求內容。

兩個消費者時,要在第一次讀取前 clone#

簽章驗證是一個常見例子:你可能需要用原始 bytes 計算簽章,同時又要把表單欄位交給 route。這時要在任何 reader 執行前先建立各自的 clone:

export default {
async fetch(request: Request) {
const formRequest = request.clone();
const signatureRequest = request.clone();
const form = await formRequest.formData();
const rawBody = await signatureRequest.text();
const signature = request.headers.get('x-signature');
const valid = await verifySignature(rawBody, signature);
if (!valid) {
return new Response('Invalid signature', { status: 401 });
}
return Response.json({ message: form.get('message') });
},
};

clone 必須發生在第一次 json()text()formData() 之前;在 body 已被讀取後才 clone,不能恢復原本的 stream。兩個 clone 也不是免費的無限複製,速度不同的 consumer 可能讓尚未讀取的資料排在記憶體佇列中。

因此,若只有 logger 需要看內容,優先讓 logger 接收已解析的欄位或受限制的摘要,不要為每個 middleware 都建立 clone。安全性相關的 raw-body 簽章則要依第三方服務要求的 canonicalization 規則驗證,不能只因為成功讀到文字就當成簽章正確。

大型上傳不要靠 clone 解決#

如果 request 可能包含大檔案或很長的 JSON,先問自己是否真的需要兩份完整 body。Cloudflare Workers 文件提醒,Worker 有記憶體邊界;把整個 body 讀成字串或 ArrayBuffer,再建立多個 clone,會把資料量放大。

純轉送情境可以保留 stream,把 request body 交給 upstream:

export default {
async fetch(request: Request) {
const upstream = new Request('https://api.example.com/upload', {
method: request.method,
headers: request.headers,
body: request.body,
});
return fetch(upstream);
},
};

實際轉送仍要補上 upstream authentication、允許的 header 和 method 檢查。不要先 await request.text() 記錄 log,再把同一個 request 交給 upstream;那會把串流問題從 route 內移到轉送邊界。

如果你同時需要檢查內容和轉送,請改設計成有大小上限的串流 transform,或先明確限制可接受的 body size。clone() 不是繞過平台 request body limit 的方法;上傳被平台以 413 拒絕,是另一條排查路徑。

Response body 也有相同規則#

不要只修 Request 而漏掉 upstream Response。以下程式在讀取 response 後,又想把同一個 response 回傳給瀏覽器,也會遇到 body 已被讀取:

const upstreamResponse = await fetch(url);
const text = await upstreamResponse.text();
return upstreamResponse;

如果真的需要同時檢查和回傳,先 clone 一份給 audit:

const upstreamResponse = await fetch(url);
const responseForAudit = upstreamResponse.clone();
const statusText = await responseForAudit.text();
console.log({ statusText });
return upstreamResponse;

同樣不要把 clone 當成每層 middleware 的預設動作。先畫出 body 的 ownership:哪一層解析、哪一層驗證、哪一層轉送,再決定單次解析、有限 clone 或串流。

用原本失敗的路徑驗證#

修好後不要只發一個最小 GET。至少測試:

  1. 小型 JSON:確認 parser 和商業邏輯都收到同一份資料。
  2. 表單:穿過所有 middleware,再確認 route 沒有第二個 reader。
  3. 錯誤簽章:確認驗證失敗時不會把含敏感資料的 raw body 寫進 log。
  4. 較大的 body:確認採用串流或明確的大小限制,而不是無上限 clone。
  5. Response audit:若有讀取 upstream response,確認回傳的 response 使用另一份仍未消耗的 stream。

bodyUsed 在 route 一開始就是 true,把修正往前移到 middleware 邊界;若 body 只讀一次卻收到 413,改查 request body size 和帳號方案,不要繼續調整 clone。

這次要記住的事#

request body 是一次性資料流,不是可以在每個函式中重複呼叫的 JSON 變數。能解析一次就傳遞結果;需要兩個獨立 reader 時,從第一次讀取前就建立有限的 clone;大型資料則維持串流。把 body ownership 寫清楚,比看到 TypeError 後到處加 clone 更可靠。

常見問題#

Q: 為什麼 Cloudflare Workers 說 request body 已經被使用?#

A: 前面的程式已呼叫 request.json()request.formData()request.text() 或其他 body-reading method,消耗了這個一次性 stream。先找第一個 reader,再把解析結果傳下去,或在第一次讀取前建立 clone。

Q: 可以在 request.json() 之後才呼叫 request.clone() 嗎?#

A: 不行。clone 要在 body 被消耗前建立;讀取之後的 clone 不能把原本的 stream 倒帶。若只有一個後續流程需要資料,改成由第一個 parser 傳遞 payload。

Q: 每個 middleware 都 clone 一份 request 會比較安全嗎?#

A: 不會。多餘的 clone 會增加資料佇列與記憶體壓力,也讓 body ownership 更難追蹤。先指定一個解析層;只有簽章驗證、原始 body 與解析資料確實需要不同 reader 時,才建立有限的 clone。

Q: clone 可以解決 Cloudflare Workers 的 413 嗎?#

A: 不行。clone 只改變 body 的讀取方式,不會提高平台 request body limit。413 要另外查 request 大小、Cloudflare 帳號方案與上游限制。

參考資料:

Cloudflare Workers:Request runtime API

Cloudflare Workers:Turnstile with Workers 的 request body 範例

Cloudflare Workers:Streams API

MDN:Request.clone()

Cloudflare Workers request body 已被讀取怎麼修?先找第一個 reader 再 clone
https://laplusda.com/posts/cloudflare-workers-request-body-already-used/
作者
Zero
發佈於
2026-08-16
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。