Workers Request 與 Response:處理 HTTP 請求與回應 | Cloudflare 完整教學

2026/08/05
Workers Request 與 Response:處理 HTTP 請求與回應 | Cloudflare 完整教學

Cloudflare Workers 的世界裡,每一次請求最終都歸結為兩個物件:進來的 Request 與回去的 Response。這一篇我們深入 fetch handler 最核心的這兩個角色——請求進來長什麼樣、回應怎麼構造、Headers 與 Body 如何操作、如何依 path 與 method 路由,以及 request.cfctx.waitUntil 這兩個 Workers 專屬的實用工具。全部基於 Web 標準 API。

前言

上一篇《定價與限制》我們把 Workers 的錢與邊界畫清楚了——CPU 時間怎麼算、Free 與 Paid 差在哪、什麼工作不該放上 Worker。文章最後我留了一句預告:接下來要把鏡頭拉近到 Worker 每天處理的最基本物件。 這一篇就是這個系列 Runtime API 段的第一篇,主角就是 fetch handler 手上的兩張牌:Request 與 Response。

先給一句話定義:Request 與 Response 是 Workers 處理 HTTP 的兩個核心 Web 標準物件——Request 封裝「進來的請求」(方法、URL、標頭、內文),Response 封裝「回去的回應」(狀態碼、標頭、內文)。 關鍵在於它們不是 Cloudflare 自己發明的:Workers 直接採用 WinterCG 與 Fetch Standard 定義的同一套 API,也就是你在瀏覽器 fetch()、Service Worker 裡用過的那套。這代表你的既有知識可以無痛遷移。

打個比方:fetch handler 就像一間餐廳的出餐口。 Request 是客人遞進來的點餐單(上面寫著要什麼餐點、有什麼備註、從哪桌來的),你的 Worker 是廚房,Response 則是你端出去的餐盤(裝了餐點、貼了標籤、標了溫度)。這一篇要教的,就是怎麼看懂點餐單(讀取 query、body、method)、怎麼依單分流到不同爐台(routing),以及怎麼把餐盤擺得漂亮又正確(設定狀態碼、Headers、回傳 JSON)。

讀完本篇,你會掌握:

  • Request 物件的完整解讀——methodurlheaders、body 的四種讀法(json/text/formData/arrayBuffer),以及 URL/URLSearchParams 怎麼拆 query
  • Response 物件的正確構造——狀態碼、自訂 Headers、回傳 JSON 的標準寫法,以及 CORS 標頭怎麼加
  • 手寫一個小型路由器——依 pathmethod 把請求分流到不同處理函式
  • 兩個 Workers 專屬工具——request.cf 取地理與網路元資料、ctx.waitUntil 在回應後做背景工作
  • 三個最容易踩的坑——body 只能讀一次、忘記回傳 Response、漏掉 CORS 標頭

核心概念

fetch handler 與請求生命週期

每個處理 HTTP 的 Worker,都以 ES Module 形式匯出一個 fetch handler。它接收三個參數,這三個參數幾乎貫穿你所有的 Workers 程式碼:

參數型別說明
requestRequest傳入的 HTTP 請求物件(Web 標準)
envEnv所有 Bindings(KV、D1、R2…)與環境變數
ctxExecutionContext生命週期控制(waitUntilpassThroughOnException

一個請求的生命週期非常直觀:請求抵達邊緣節點 → Cloudflare 建立 Request 物件並呼叫你的 fetch handler → 你的程式碼讀取請求、做運算或 I/O → 你 return 一個 Response → Cloudflare 把回應送回給用戶端。 這裡有一條鐵律:fetch handler 必須回傳一個 Response(或解析為 Response 的 Promise)。 忘了回傳,Worker 就會報錯——這是新手最常見的坑之一,後面會專門講。

// src/index.ts —— 最小可執行的 fetch handler
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    return new Response("Hello, Cloudflare Workers!", { status: 200 });
  },
} satisfies ExportedHandler<Env>;

Request:讀懂進來的請求

Request 物件封裝了這次 HTTP 請求的全部資訊。最常用的幾個屬性:

  • request.method:HTTP 方法字串,如 "GET""POST""DELETE"。routing 時用它區分「讀」還是「寫」。
  • request.url:完整的請求 URL 字串(含 protocol、host、path、query)。注意它是字串,要拆解 path 與 query 得先用 new URL(request.url) 包一層。
  • request.headers:一個 Headers 物件,用 headers.get("Content-Type") 取值。
  • request body:請求內文,只能讀一次(下一節詳述)。

Web 標準 URL / URLSearchParams 是拆解請求路徑與查詢字串的正確工具,別自己用 split("?") 土法煉鋼:

const url = new URL(request.url);
url.pathname;                       // "/api/users"        ← 用來路由
url.searchParams.get("page");       // "2"                 ← 讀單一 query 參數
url.searchParams.get("missing");    // null                ← 不存在回傳 null
url.searchParams.getAll("tag");     // ["a", "b"]          ← 同名多值

讀取 body:json / text / formData(只能讀一次)

Request 的 body 是一個 ReadableStream(可讀串流),這帶來一個必須刻進肌肉記憶的規則:body 只能被讀取一次。 讀 body 有幾種便利方法,各自對應不同的 Content-Type:

方法回傳適用情境
await request.json()解析後的物件Content-Type: application/json
await request.text()字串純文字、原始 payload
await request.formData()FormData 物件表單提交(含檔案上傳)
await request.arrayBuffer()ArrayBuffer二進位資料(圖片、檔案位元組)

一旦你呼叫其中任何一個,串流就被消耗掉了,第二次再讀(無論同方法或不同方法)都會拋出 body has already been used 的錯誤。要重複使用,把結果存進變數;要真的讀兩次,先 request.clone()

Response:構造回去的回應

Response 也是 Web 標準物件。建構子簽名是 new Response(body, init):

  • body:可以是字串、ReadableStreamArrayBuffernull 等。
  • init:一個選項物件,常用 status(狀態碼,預設 200)、statusTextheaders

回傳 JSON 有兩種寫法。傳統寫法是自己 JSON.stringify 並設好 Content-Type;新版 runtime 也支援標準的靜態方法 Response.json(data),會自動序列化並補上 JSON 標頭:

// 寫法一(傳統,控制力最高)
return new Response(JSON.stringify({ ok: true }), {
  status: 200,
  headers: { "Content-Type": "application/json; charset=utf-8" },
});

// 寫法二(Web 標準便利方法)
return Response.json({ ok: true });

request.cf:Workers 專屬的邊緣元資料

這是 Workers 在標準 Request 之上加的非標準擴充,也是它最實用的殺手級功能之一。request.cf 帶著這次請求的地理與網路元資料,讓你在邊緣就能做在地化、風控、路由決策:

request.cf 欄位說明範例值
countryISO 3166-1 兩字母國碼"TW"
city / region城市 / 地區名稱"Taipei"
colo邊緣節點機場 IATA 代碼"TPE"
httpProtocolHTTP 版本"HTTP/2"
tlsVersionTLS 版本"TLSv1.3"

提醒:request.cf 是 Cloudflare 專屬屬性,在本機 wrangler dev 有時欄位會不完整或為 undefined,撰寫程式時務必做好防呆(如 request.cf?.country ?? "unknown")。

ctx.waitUntil:回應送出後的背景工作

ctx.waitUntil(promise) 讓你在回應已經 return 之後繼續跑非同步工作,不拖慢使用者拿到回應的時間。典型場景:寫分析日誌、更新快取、發通知——這些「使用者不需要等」的事,全丟給 waitUntil。它最多可把 Worker 的存活時間延長約 30 秒。切記別解構 ctxconst { waitUntil } = ctx 會導致 Illegal invocation 錯誤。

實作範例

概念講完,我們把上面所有零件組成一個可執行的小型 API:它依 path 與 method 路由、讀 query 與 body、回傳 JSON、加上 CORS 標頭,並用 ctx.waitUntil 在背景記錄請求。

一個手寫的小型路由器

// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const { pathname } = url;
    const method = request.method;

    // ① 依 path + method 路由
    if (method === "GET" && pathname === "/api/hello") {
      return handleHello(request);
    }
    if (method === "POST" && pathname === "/api/users") {
      return handleCreateUser(request, ctx);
    }
    if (method === "GET" && pathname === "/api/whoami") {
      return handleWhoAmI(request);
    }

    // ② 沒有匹配的路由 → 一定要回傳 Response,不能什麼都不回
    return jsonResponse({ error: "Not Found" }, 404);
  },
} satisfies ExportedHandler<Env>;

// 統一的 JSON 回應工具:自動序列化 + 補上 JSON 與 CORS 標頭
function jsonResponse(data: unknown, status = 200): Response {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      "Content-Type": "application/json; charset=utf-8",
      "Access-Control-Allow-Origin": "*", // CORS:允許跨來源存取
    },
  });
}

讀取 query 參數

// GET /api/hello?name=Ben  →  { "message": "Hello, Ben!" }
function handleHello(request: Request): Response {
  const url = new URL(request.url);
  const name = url.searchParams.get("name") ?? "World"; // 不存在時給預設值
  return jsonResponse({ message: `Hello, ${name}!` });
}

讀取 JSON body 並用 ctx.waitUntil 記錄

// POST /api/users  body: { "name": "Ben", "email": "ben@example.com" }
async function handleCreateUser(request: Request, ctx: ExecutionContext): Promise<Response> {
  // 先驗證 Content-Type,避免對非 JSON 內文呼叫 .json() 而爆錯
  const contentType = request.headers.get("Content-Type") ?? "";
  if (!contentType.includes("application/json")) {
    return jsonResponse({ error: "Content-Type must be application/json" }, 415);
  }

  let body: { name?: string; email?: string };
  try {
    body = await request.json(); // body 只能讀這一次
  } catch {
    return jsonResponse({ error: "Invalid JSON body" }, 400);
  }

  if (!body.name) {
    return jsonResponse({ error: "name is required" }, 422);
  }

  // 背景工作:記錄這次請求,不拖慢回應(回應會先送出)
  ctx.waitUntil(logToAnalytics(body.name));

  // 立刻回傳,不等背景任務完成
  return jsonResponse({ created: true, name: body.name }, 201);
}

// 模擬一個耗時的背景寫入(使用者不需要等它)
async function logToAnalytics(name: string): Promise<void> {
  await fetch("https://analytics.example.com/log", {
    method: "POST",
    body: JSON.stringify({ event: "user_created", name }),
  });
}

讀取 request.cf 做在地化回應

// GET /api/whoami  →  依請求來源回傳國家、城市、邊緣節點
function handleWhoAmI(request: Request): Response {
  const cf = request.cf; // Workers 專屬屬性,本機 dev 可能為 undefined
  return jsonResponse({
    country: cf?.country ?? "unknown",   // 例:"TW"
    city: cf?.city ?? "unknown",         // 例:"Taipei"
    colo: cf?.colo ?? "unknown",         // 例:"TPE"(就近的邊緣節點)
    tlsVersion: cf?.tlsVersion ?? "unknown",
  });
}

處理 formData(表單與檔案上傳)

當前端用 multipart/form-data 提交表單,改用 request.formData() 讀取:

// POST /api/upload  (Content-Type: multipart/form-data)
async function handleUpload(request: Request): Promise<Response> {
  const form = await request.formData(); // 一樣只能讀一次
  const title = form.get("title");       // 一般欄位:字串
  const file = form.get("file");         // 檔案欄位:File 物件

  if (!(file instanceof File)) {
    return jsonResponse({ error: "file field is required" }, 422);
  }

  return jsonResponse({
    title,
    filename: file.name,
    size: file.size, // 位元組數
  });
}

常見錯誤與最佳實踐

坑一:對同一個 body 讀取兩次,觸發 body has already been used

這是操作 Request/Response 最經典的錯誤。body 是只能讀一次的串流,一旦 await request.json()(或 .text().formData())之後,再讀就爆。最常見的情境是「先讀一次做驗證,又讀一次做解析」。正確做法有三種:第一,一次讀完存進變數,後續都用變數——const body = await request.json(); 之後全用 body;第二,若要對原始字串做多種處理(例如先算 HMAC 簽章驗證 Webhook、再解析),先 const raw = await request.text();,之後都對 raw 操作,不要再碰 request;第三,若你要轉發原始請求給另一個服務,用 const clone = request.clone(); 複製一份,原件和複本各自有獨立的 body 可讀。

坑二:忘記回傳 Response,或在某些分支「掉出去」沒回傳。

fetch handler 必須回傳一個 Response。新手最常見的失誤是:路由 if 判斷寫了一堆,卻沒有在所有路徑都 return,結果某個未匹配的請求「掉出」所有分支,handler 回傳了 undefined,Worker 直接報 The script will never generate a response正確做法:一律在路由結尾放一個「兜底」回應,例如 return jsonResponse({ error: "Not Found" }, 404);,確保任何請求都有明確結果;另外每個 async 分支函式的所有 code path 也都要 return Response,別讓 try/catch 的 catch 分支忘了回傳。

坑三:漏掉 CORS 標頭,前端瀏覽器呼叫被擋。

當你的 Worker 提供 API 給瀏覽器端的網頁(尤其跨網域)呼叫,少了 CORS 標頭,請求會被瀏覽器的同源政策擋下,Console 出現 blocked by CORS policy正確做法:在回應加上 Access-Control-Allow-Origin(如上面 jsonResponse 工具所示),生產環境建議指定明確網域而非 *;此外,瀏覽器對非簡單請求(帶自訂標頭、PUT/DELETE 等)會先發一個 OPTIONS 預檢(preflight)請求,你的路由要專門處理 OPTIONS,回傳 204 並帶上 Access-Control-Allow-MethodsAccess-Control-Allow-Headers,否則預檢失敗、正式請求根本發不出去。

// 在 fetch handler 最前面處理 CORS 預檢
if (request.method === "OPTIONS") {
  return new Response(null, {
    status: 204,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
    },
  });
}

額外提醒:Headers 大小寫不敏感,但 body 與 status 有預設值陷阱。 headers.get("content-type")headers.get("Content-Type") 等價,不用糾結大小寫;但要記得 new Response() 若不給 status 預設是 200,若你想回錯誤卻忘了設 status,前端會誤以為成功。回應錯誤時務必明確指定對應的狀態碼(400/404/422/500 等)。

小結

上一篇《定價與限制》我們把 Workers 的錢與邊界量化清楚——CPU 時間怎麼算、Free 與 Paid 差在哪、什麼工作不該放上 Worker。這一篇,我們正式踏進 Runtime API 段,把 Worker 每天處理的兩個最基本物件拆開來看:

  • Request 與 Response 是 Web 標準物件——和瀏覽器 fetch() 同一套 API,既有知識可無痛遷移;Workers 只在其上做了少量擴充(如 request.cf)。
  • 讀取請求的完整工具箱——method/url/headers 取基本資訊,new URL() + searchParams 拆 query,json/text/formData/arrayBuffer 四種方法讀 body。
  • body 只能讀一次——這是串流本質決定的鐵律,重複讀要靠變數快取或 request.clone()
  • 構造回應的正確姿勢——new Response(body, init) 設好 statusheaders,回 JSON 用 Response.json() 或手動 JSON.stringify,別忘了 CORS。
  • 兩個 Workers 專屬工具——request.cf 在邊緣就能拿到國家/城市/節點資訊,ctx.waitUntil 讓你在回應送出後做背景工作而不拖慢延遲。

掌握了 Request 與 Response,你已經能寫出一個功能完整的邊緣 API 了。但有個問題會立刻浮現:如果同一個請求每次都要重新運算或重新抓資料,不是很浪費嗎? 邊緣運算最迷人的優勢之一,就是能把運算結果快取在離使用者最近的節點。下一篇《Cache API》,我們會深入 Workers 的邊緣快取機制——caches.default 怎麼用、快取優先(cache-first)模式怎麼寫,以及為什麼 ctx.waitUntil 會是「非同步寫快取不阻擋回應」的最佳拍檔。認識了請求與回應這對基本功,接下來就讓它們跑得更快。

想深入官方 Runtime API 細節,可以隨時參考 Cloudflare 官方 Request 文件Response 文件。準備好讓你的 Worker 飛快回應了嗎?我們下一篇《Cache API》見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →