响应

H3 响应实用工具。

#事件流

#EventStream()

#isEventStream(input)

#清理

#sanitizeStatusCode(statusCode?, defaultStatusCode)

确保状态码是有效的 HTTP 状态码。

#sanitizeStatusMessage(statusMessage)

确保状态消息安全可用于响应。

允许的字符:水平制表符、空格或可见 ASCII 字符:https://www.rfc-editor.org/rfc/rfc7230#section-3.1.2

#静态资源服务

#serveStatic(event, options)

根据请求路径动态提供静态资源。

安全性——路径遍历: serveStatic 会解析 ./.. 片段,但会故意将编码后的分隔符(%2f、%5c)在传递给 getMeta/getContents 的 id 中保持百分号编码状态,这与 event.url.pathname 的行为完全一致。因此,id 具有与路由器和按路径限定范围的 use() 守卫所匹配的相同片段结构:/private%5cx 会保持为一个不透明片段,不会绕过 use("/private/**") 守卫而作为 /private/x 提供服务。请将 id 作为不透明字符串解析到资源根目录中——对其进行解码的后端会重新引入分隔符,从而重新打开漏洞。

非规范路径名不会被提供服务(返回 404,或在设置了 fallthrough 时继续处理):包括多个前导分隔符(//private/x、/\\private/x),或 URL 规范化后仍保留的点片段,也就是使用 %25 嵌套转义表示的路径(/pub/%252e%252e/private/x)。这两种情况都会在缺少更具体的 use("/private/**") 守卫时分派到捕获所有路由,而 serveStatic 能够从中构建的唯一 id 会解析回受保护的路径。资源只能通过其规范拼写访问——也就是路由和 use() 守卫所匹配的拼写。

其他所有内容都会被解码一次,用于磁盘上的查找,因此文件的真实名称可以传递给后端:/50%25.png → /50%.png、/a%20b → /a b,并且嵌套分隔符中的一层 %25 会被剥离(/a%252fb → /a%2fb,仍然是字面量 %2f,绝不会成为边界)。RFC 3986 的保留字符集会保持编码状态,因此 id 绝不会增长出 ? 或 #,也就不会在 URL 中截断它。

对于基于文件系统的资源,serveStatic 有两件事无法强制执行:不区分大小写的文件系统(macOS、Windows)需要将允许/拒绝检查的两侧都进行大小写折叠(否则 /SECRET.env 会绕过针对 /secret.env 的检查);而对于符号链接,需要在跟随链接后再次确认解析出的路径位于资源根目录下(例如 realpath(target))。

#更多响应实用工具

#html(first)

以 HTML 内容响应。

示例:

app.get("/", (event) => html(event, "<h1>Hello, World!</h1>"));

#iterable(iterable)

遍历一个数据块源,按顺序发送每个数据块。支持异步操作与数据块的混合发送。

每个数据块必须是字符串或缓冲区。

对于生成器(yield)函数,返回值与 yield 的值处理方式相同。

第一个数据块会在创建响应之前等待完成,因此在生成该数据块期间暂存的状态和标头(event.res.status、event.res.headers)仍会生效。第一个数据块之后设置的所有内容都会被忽略——届时标头已经发送到线路上。(返回原始 ReadableStream 不存在这样的时间窗口:它的响应会在读取流之前创建。)

示例:

return iterable(async function* work() {
  // 打开文档主体
  yield "<!DOCTYPE html>\n<html><body><h1>Executing...</h1><ol>\n";
  // 执行工作 ...
  for (let i = 0; i < 1000; i++) {
    await delay(1000);
    // 报告进度
    yield `<li>Completed job #`;
    yield i;
    yield `</li>\n`;
  }
  // 关闭报告
  return `</ol></body></html>`;
});
async function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

#noContent(status)

响应一个空负载。

示例:

app.get("/", () => noContent());

#onDispose(event, cb)

注册一个回调,在事件完全结束后运行:响应正文完成流式传输、客户端断开连接或正文发生错误——适用于所有运行时环境,而不仅仅是 Node.js。

回调在正常完成时接收 undefined,否则接收取消/中止原因。回调按照注册顺序、在全局 onResponse 钩子之后运行;同步抛出和异步拒绝会被吸收(除非应用配置了 silent,否则会通过 console.error 报告),待处理的异步回调会传递给 waitUntil。

在事件销毁后注册会立即调用该回调。只有在请求处理期间(处理器、中间件或 onResponse 中)注册时,才能保证观察到事件的结束。

注意:这表示的是 “h3 已完成对该事件的处理”,而不是 “客户端已收到响应” ——对于非 Node.js 运行时中的非流式正文,它会在响应交给运行时后触发。若要在仍然生成响应期间对客户端断开连接作出反应(例如中止上游 fetch),请改用 event.req.signal。

示例:

app.get("/sse", (event) => {
  const interval = setInterval(() => {}, 1000);
  onDispose(event, () => clearInterval(interval));
  // ... 返回一个流式响应
});

#raw(value)

将字符串标记为可信的、已预转义的 HTML,使其被 {@link html} 工具使用时不会再次转义。

仅对完全由你控制的标记使用此方法——将用户输入传递给 raw 会重新引入 XSS 风险。

示例:

// `heading` 是可信的标记;`userName` 会自动转义。
app.get("/", () => html`<div>${raw(heading)}<span>${userName}</span></div>`);

示例:

// 原样发送可信的标记字符串:
app.get("/", () => html(raw("<h1>Hello, World!</h1>")));

#redirect(location, status, statusText?)

向客户端发送重定向响应。

它会在响应中添加 location 头,默认状态码为 302。

响应体发送一个简单的 HTML 页面,包含 meta 刷新标签,以防客户端忽略响应头时进行重定向。

安全性: 如果 location 来源于用户输入(查询参数、表单字段、标头等),请在重定向前根据允许的目标列表对其进行验证。未经检查地传递用户控制的值会导致开放重定向漏洞。对于“返回上一页”的流程,建议使用 redirectBack,它只接受同源的 referer。

示例:

app.get("/", () => {
  return redirect("https://example.com");
});

示例:

app.get("/", () => {
  return redirect("https://example.com", 301); // 永久重定向
});

#redirectBack(event)

使用 referer 头将客户端重定向回上一个页面。

如果 referer 头缺失或是不同的来源,则回退到提供的 URL(默认为 "/")。

默认情况下,只使用 referer 的 pathname(查询字符串和哈希被剥离)以防止伪造的 referer 携带意外参数。设置 allowQuery: true 以保留查询字符串。

安全性: fallback 值必须是一个可信的硬编码路径——绝不要使用用户输入。将用户控制的值(例如查询参数)作为 fallback 会导致开放重定向漏洞。

示例:

app.post("/submit", (event) => {
  // 处理表单...
  return redirectBack(event, { fallback: "/form" });
});

#writeEarlyHints(event, hints)

向客户端写入 HTTP/1.1 103 Early Hints。

在本身不支持早期提示的运行时环境中,此函数会退回到设置响应头,这些响应头可以被 CDN 使用。