H3 发送响应
H3 的响应模型只有一条规则:要么 return 一个值,要么 throw 一个错误。没有 res.send(),没有 ctx.body = ...,也不需要手动结束响应。
规则简单,但「返回什么」会决定响应长什么样。这一节把返回值到 Response 的转换规则全部列清,并说明两个必须提前知道的坑:状态码会被丢弃、错误响应里拿不到准备头。
返回值会被自动转换
返回一个对象,H3 会把它变成带 JSON 头的完整响应:
// 返回对象 → 自动 JSON 序列化并设置 content-type
const handler = defineHandler((event) => ({ hello: "world" }));
行为上等价于你手写这段:
// H3 内部实际做的事(示意)
const handler = (event) =>
new Response(JSON.stringify({ hello: "world" }), {
headers: {
"content-type": "application/json;charset=UTF-8",
},
});
返回 Promise 或 async 函数的返回值时,H3 会等它 resolve 后再发响应;中途 throw 的错误会交给错误处理(见 2.6)。
🚀 实现上,Node.js 运行时用的是 srvx 的
FastResponse做优化,比手工拼Response少一层感知不到但确实存在的开销。
先准备状态与头部
在 return 之前,可以通过 event.res 改状态码与响应头:
defineHandler((event) => {
event.res.status = 201; // 创建成功
event.res.statusText = "Created";
event.res.headers.set("Content-Type", "text/html");
return "<h1>Hello, World</h1>";
});
⚠️ 坑一:返回完整的
Response时,event.res.status会被丢弃。 头部会合并(以你返回的Response为准覆盖),状态码则完全以Response里的为准。所以如果你返回的是new Response(...),状态码要写在Response里,别写在event.res上。
坑二:一旦抛错,准备好的状态与头部全部丢弃。 错误响应里要带头部,只能用new HTTPError({ headers })。对于那些「在错误发生前就必须先设好」的头(典型是 CORS),可以退一步用event.res.errHeaders,它会被自动合并进错误响应。
各类返回值的转换规则
下图是「返回什么」到「怎么发」的全部分支,后面按类别逐段说明:

| 返回类型 | 实际发送方式 | 自动设置的头部 |
|---|---|---|
| object / array / number / boolean | JSON.stringify 后发送 |
application/json |
| string | 纯文本 | 未设 content-type 时默认 text/plain;charset=UTF-8 |
Response |
原样作为最终响应 | 无(你自己设) |
ReadableStream / Node Readable |
按流发送 | 无 |
ArrayBuffer / Uint8Array / Buffer |
二进制发送 | content-length |
Blob |
按流发送 | content-type、content-length |
File |
按流发送 | content-type、content-length、content-disposition |
null / undefined |
空响应体 | 无 |
JSON 可序列化的值
对象、数组、数字、布尔值都会走 JSON.stringify:
// 返回对象 → application/json
app.get("/api/user", (event) => ({ id: 1, name: "张三" }));
// 返回数组也走同一条路径
app.get("/api/list", (event) => [1, 2, 3]);
如果对象自带 .toJSON(),序列化行为由它决定——这是控制日期格式、隐藏敏感字段的标准做法。
字符串
字符串按纯文本发。要发 HTML,得自己改 content-type:
app.get("/", (event) => {
// 字符串默认按纯文本发,想发 HTML 必须自己声明类型
event.res.headers.set("Content-Type", "text/html;charset=UTF-8");
return "<h1>hello world</h1>";
});
拼 HTML 更安全的写法是用 html 标签模板,插值会被自动转义,挡掉大部分 XSS:
import { html, raw } from "h3";
// 标签模板:${} 里的内容自动做 HTML 转义
app.get("/hello/:name", (event) => html`<h1>hello ${event.context.params.name}</h1>`);
// 确认可信的片段用 raw() 包一层,原样输出
app.get("/", () => html(raw("<h1>hello world</h1>")));
❗ 用普通字符串调用
html("...")(而不是标签模板)会把整串都转义,若转义改变了内容,H3 会打一条警告。动态值走标签模板,可信片段用raw(),两者别混。
Response
返回一个完整的 Response 时,它就是最终响应:
// 返回完整 Response:它就是最终响应,event.res.status 不再生效
app.get("/", (event) =>
new Response("Hello, world!", { headers: { "x-powered-by": "H3" } }),
);
此时前面准备的头部会作为默认值合并进来,但 event.res.status / statusText 被忽略。有一条细节值得记住:返回的 Response 状态码 ≥ 400 时,准备头会被丢弃(和抛错一样,只合并 event.res.errHeaders);状态码 < 400 的成功与重定向响应则会带上全部准备头——所以在返回 302 之前调用 setCookie() 是有效的。
流与二进制
ReadableStream、Node 的 Readable、Blob、File 都按流发送,二进制类型(ArrayBuffer、Uint8Array、Buffer)会自动补 content-length:
// 直接返回流,H3 负责分块推给客户端
app.get("/stream", (event) => {
return new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("第一块\n"));
controller.close();
},
});
});
完整的分块输出示例见 7.6。
特殊返回值
null 与 undefined
两者都发空响应体,状态码 200。handler 里没有 return 语句,等价于 return undefined——这正是「接口返回 200 但内容为空」的成因。
Error
返回一个 Error 实例也会被发送,但官方明确建议不要这么做:
// 不推荐:错误变成普通返回值,外层拦截不到了
app.get("/bad", () => new Error("出错了"));
正确做法是 throw,这样错误才能从任意嵌套的工具函数里向上传播,被统一的错误处理接住。
BigInt
BigInt 会被转成字符串发送。要注意它不能直接放进 JSON 对象——JSON.stringify 遇到 BigInt 会抛错,需要自己实现 .toJSON()。
Symbol 与 Function
返回 Symbol 或 Function 的行为未被确定:当前版本会发一个类似字符串的表示,但未来版本可能改为抛错。不要把这种写法用进业务代码。
有两个内部 Symbol 有明确含义:
| Symbol | 含义 |
|---|---|
Symbol.for("h3.notFound") |
表示没有匹配到路由,触发 404 |
Symbol.for("h3.handled") |
表示请求已被处理,H3 不应继续(Node.js 专用) |
一张排查表
| 现象 | 最可能的原因 |
|---|---|
| 接口返回 200 但内容为空 | handler 漏了 return,或返回了 undefined |
设了 event.res.status = 404 但客户端收到 200 |
返回的是完整 Response,状态码被 Response 覆盖 |
| 抛错后响应里没有 CORS 头 | 准备头在错误路径被丢弃,需 HTTPError({ headers }) 或 event.res.errHeaders |
| 返回 HTML 却被当纯文本显示 | 没设 content-type: text/html |
JSON.stringify 报 BigInt 错误 |
返回对象里含 BigInt,需实现 .toJSON() |
延伸阅读
- 错误的正确表达方式与错误 JSON 结构:见 2.6 错误处理
- 流式响应的完整示例:见 7.6
- 静态资源与文件下载:见 7.5
event.res的完整字段:见 4.2 H3Event 事件对象

TRAE-AI编程


更多建议: