Request 与 Response 对象详解:请求里有什么,响应怎么写
读完这一篇,你能做到两件事:拿到一个 Request 时清楚它身上有哪些字段可以取、哪些字段在 Workers 里有额外内容;构造 Response 时知道状态码、响应头、流式 body 分别怎么写,不会再写出"看起来返回了 JSON 但状态码是 200 的空壳"这种代码。下面的返回值都来自 2026-11-10 在本机部署的一个临时 Worker,验证完已经删掉。
1. Request 身上的六样东西
fetch(request) 收到的 request 是标准 Request 实例,Workers 在它上面加了边缘相关的属性。常用的部分是这些:
| 成员 | 类型 | 说明 |
|---|---|---|
request.url | string | 完整 URL,含 query,需要自己 new URL() 拆 |
request.method | string | 大写方法名,GET / POST / OPTIONS |
request.headers | Headers | 只读视图,取头用 get() |
request.cf | object | Cloudflare 边缘元数据,本地 wrangler dev 下是简化值 |
request.body | ReadableStream | 原始字节流,一般不直接用 |
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 |
| 多个 Cookie | headers.append("set-cookie", ...) 两次 | 两个 set-cookie 并存 |
| 流式 body | new 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 运行时 API | https://developers.cloudflare.com/workers/runtime-apis/request/ | Request 字段清单(页面正文未完整提取,仅取入口) |
| Workers Response 运行时 API | https://developers.cloudflare.com/workers/runtime-apis/response/ | Response.json / Response.redirect 静态方法(未抓取) |
| MDN Fetch API | https://developer.mozilla.org/docs/Web/API/Response | Headers.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的文档描述与环境常识,本次未单独构造对比测试。