H3 发送响应

2026-10-30 15:38 更新

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,它会被自动合并进错误响应。

各类返回值的转换规则

下图是「返回什么」到「怎么发」的全部分支,后面按类别逐段说明:

handler 返回值到响应类型的六条分派路径

返回类型 实际发送方式 自动设置的头部
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 事件对象
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号