KernelRanch
返回文章列表

Request 与 Response 对象详解:请求里有什么,响应怎么写

2026-11-10 #Cloudflare

读完这一篇,你能做到两件事:拿到一个 Request 时清楚它身上有哪些字段可以取、哪些字段在 Workers 里有额外内容;构造 Response 时知道状态码、响应头、流式 body 分别怎么写,不会再写出"看起来返回了 JSON 但状态码是 200 的空壳"这种代码。下面的返回值都来自 2026-11-10 在本机部署的一个临时 Worker,验证完已经删掉。

1. Request 身上的六样东西

fetch(request) 收到的 request 是标准 Request 实例,Workers 在它上面加了边缘相关的属性。常用的部分是这些:

成员类型说明
request.urlstring完整 URL,含 query,需要自己 new URL() 拆
request.methodstring大写方法名,GET / POST / OPTIONS
request.headersHeaders只读视图,取头用 get()
request.cfobjectCloudflare 边缘元数据,本地 wrangler dev 下是简化值
request.bodyReadableStream原始字节流,一般不直接用
request.clone()方法复制一份独立 body,用于两边都要读的场景

headers 是大小写不敏感的,get("User-Agent") 和 get("user-agent") 结果一样。cf 对象在线上才有完整字段,country、city、colo、asn、tlsVersion 这些都在里面,取之前加个可选链更稳。

取查询参数不用解析字符串,new URL(request.url).searchParams 就是标准 URLSearchParams:

const url = new URL(request.url);
const q = url.searchParams.get("q");          // 单个
const all = Object.fromEntries(url.searchParams); // 全部转对象

实测 /echo?x=1&y=2 返回:

{"method":"GET","path":"/echo","query":{"x":"1","y":"2"},
 "ua":"Mozilla/5.0 (Windows NT 10.0; ...) PowerShell/7.6.3",
 "ip":"218.188.154.160","country":"HK","colo":"HKG"}

ip 来自 Cloudflare 注入的 cf-connecting-ip 头,客户端改不了它;country 和 colo 来自 request.cf。这三个值合起来足够做地域判断和简单风控,不必再接 IP 库。关于边缘元数据在第一个站点里的用法,见 53-first-free-site-with-workers.md。

2. 读 body:三种解析方式,只能选一次

request.text()、request.json()、request.formData() 都是消费型操作,读完流就空了。这一点在文档里写着,实际踩的时候报错长这样——我把"先 text 再 json"写进一个端点,线上返回:

TypeError: Body has already been used. It can only be used once.
Use tee() first if you need to read it twice.

状态码 500,消息是运行时抛的,不是框架抛的。两种解法:

// 解法一:克隆
const a = request.clone();
const b = request.clone();
const text = await a.text();
const data = await b.json();

// 解法二:只读一次,把结果往下传
const text = await request.text();
handle(JSON.parse(text));

clone() 实测可用,同一个请求克隆两份后分别 text() 和 json(),两边都拿到了正确内容:{"text":"{\"k\":1}","json":{"k":1}}。注意 clone() 要在读之前调用,读完了再克隆一样会报错。

表单提交用 formData() 最省事:

if (request.method !== "POST") {
  return new Response("need POST", { status: 405 });
}
const fd = await request.formData();
return Response.json({ name: fd.get("name") });

用 curl -d 'name=kernelranch&v=1' 打过去,返回 {"name":"kernelranch","raw":"name=kernelranch&v=1"}。方法不匹配时先返回 405,比让 formData() 在 GET 上抛异常干净。

3. Response 的五种写法

场景写法实测结果
普通文本new Response("ok")200,content-type: text/plain;charset=UTF-8
JSON + 状态码Response.json(data, { status: 201, headers })201,自动 application/json
多个 Cookieheaders.append("set-cookie", ...) 两次两个 set-cookie 并存
流式 bodynew Response(stream)客户端收到 3 个 chunk
跳转Response.redirect(url, 302)302 + location

Response.json() 是静态方法,第二个参数和 new Response() 的 init 一样。自定义头直接写在 headers 里:

return Response.json(
  { ok: true, at: new Date().toISOString() },
  {
    status: 201,
    headers: { "cache-control": "no-store", "x-api-version": "1" }
  }
);

回读响应确认:status=201,cache-control: no-store,x-api-version: 1,content-type: application/json。

多个 set-cookie 是个容易翻车的点。用对象字面量写 {"set-cookie": "a=1"} 只能设一个,第二个会覆盖第一个;要用 Headers 实例加 append:

const headers = new Headers();
headers.append("set-cookie", "a=1; Path=/; HttpOnly; Secure");
headers.append("set-cookie", "b=2; Path=/; SameSite=Lax");
return new Response("two cookies", { headers });

实测响应头里两条都在:a=1; Path=/; HttpOnly; Secure | b=2; Path=/; SameSite=Lax。

流式返回适合大文件或边算边发。ReadableStream 的 pull 被反复调用,每次 enqueue 一段,close() 结束:

let n = 0;
const stream = new ReadableStream({
  pull(controller) {
    if (n >= 3) return controller.close();
    controller.enqueue(new TextEncoder().encode(`chunk-${n++}\n`));
  }
});
return new Response(stream, { headers: { "content-type": "text/plain; charset=utf-8" } });

客户端拿到的 body 是 chunk-0\nchunk-1\nchunk-2\n,说明确实是分段发出去的,不是攒完再发。

跳转用 Response.redirect(),别手动拼头:

return Response.redirect("https://example.com/", 302);

注意 Response.redirect 只接受绝对 URL,/login 这种相对路径会抛 TypeError,要先用 new URL("/login", request.url) 补全。

4. 把上游响应原样转发

代理类需求最常见。正确写法是让 body 保持流式,不要把上游整个读进内存:

const upstream = await fetch("https://api.example.com/data", {
  headers: { authorization: `Bearer ${env.TOKEN}` }
});
return new Response(upstream.body, upstream);

第二个参数直接传 upstream,状态码和响应头会一起带过来,upstream.body 是流,边收边发。如果要改某个头,就先 new Headers(upstream.headers) 改完再传。

有一类状态码不能带 body:204、205、304。给它们传字符串会直接抛 TypeError,正确写法是 body 传 null:

return new Response(null, {
  status: 204,
  headers: { "cache-control": "no-store" }
});

OPTIONS 预检和 HEAD 请求也会走到这里,写 CORS 处理器时容易忽略。

5. 常见问题

问:request.cf 在本地为什么是 undefined 或字段很少? 答:wrangler dev 默认跑本地运行时,cf 只有部分字段的模拟值,甚至没有。要拿到真实值,部署后用线上地址访问,或者加 --remote 让请求走真实边缘。本地开发时对 cf 做空值兜底,别让它成为线上才炸的隐患。

问:返回中文乱码怎么办? 答:new Response("中文") 默认 content-type: text/plain;charset=UTF-8,一般不会乱码;但如果你手动设了 text/plain 而不带 charset,浏览器可能按 latin-1 解。显式写 text/plain; charset=utf-8 或 text/html; charset=utf-8,成本很低。

问:Response.json() 和 JSON.stringify 有什么区别? 答:Response.json() 会自动设 content-type: application/json,并处理 undefined 字段(被丢掉);手动 JSON.stringify 之后再塞进 new Response() 不会设头,需要自己加。两者对 BigInt 都会抛错,遇到大整数先转字符串。

小结

Request 是一个带边缘元数据的标准请求对象,能取的东西就六样:URL、方法、头、cf、body、clone()。Response 则是状态码 + 头 + body 三件套,JSON、流、跳转、多 Cookie 都是这三件套的不同组合。

真正会咬人的只有 body 的消费语义:读一次就没了,报错信息是 Body has already been used。写完路由回来检查一遍,凡是同一个请求读了两次的地方,要么 clone(),要么改成读一次传下去。路由和中间件的组织方式在 54-workers-fetch-handler-tutorial.md 里展开,把这些对象包进框架的写法见 58-hono-on-workers.md。

数据来源与检索时间

检索时间 2026-11-10。响应状态码、响应头、body 内容、报错文本均为本机对已部署 Worker 的真实请求结果。

来源URL对应数据
Workers Request 运行时 APIhttps://developers.cloudflare.com/workers/runtime-apis/request/Request 字段清单(页面正文未完整提取,仅取入口)
Workers Response 运行时 APIhttps://developers.cloudflare.com/workers/runtime-apis/response/Response.json / Response.redirect 静态方法(未抓取)
MDN Fetch APIhttps://developer.mozilla.org/docs/Web/API/ResponseHeaders.append 与 set-cookie 语义(未抓取)

核验说明

  • 已部署验证并删除:临时 Worker cf57-reqres(wrangler.toml + 单文件 src/index.js)于 2026-11-10 部署到 cf57-reqres.hyysn.workers.dev,上传 2.55 KiB / gzip 0.95 KiB,启动 2ms。验证结束后执行 wrangler delete cf57-reqres --force,返回 Successfully deleted。
  • 第 1、2、3 节的返回值全部来自该 Worker 的实测:/echo 的 cf 字段、/twice 的 TypeError 原文、/clone 的双读结果、/cookie 的两条 set-cookie、/stream 的三个 chunk、/json 的 201 与自定义头、/redirect 的 302、/form 的 200 与 405。
  • 官方 Request 页面本次抓取返回的是文档导航结构,正文段落没有提取到,因此字段语义以本机实测为准;Response 页面与 MDN 未抓取,表里标注了"未抓取"。
  • 未做:没有测 request.body 直接读取的流式消费,没有测 Response.redirect 的相对路径报错(该结论依据 MDN 规范描述),没有测大文件流式转发的内存占用。
  • 关于 cf 在本地开发下的具体表现,依据 wrangler dev 的文档描述与环境常识,本次未单独构造对比测试。