
H3 完全上手指南极简 HTTP 框架如何内置安全边界【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3H3 是一个可组合、运行时无关的极简 HTTP 框架核心只负责事件、路由与中间件编排请求体解析、响应构造、Cookie、会话、代理、JSON-RPC 全部以独立工具函数形式按需取用且每个函数都内置了明确的安全默认值。它解决什么问题写 HTTP 服务时痛点很少是少一个功能而是功能之间谁替我守边界。请求体多大算超限超限是预缓冲还是边读边算multipart 要不要自动嗅探x-forwarded-for第一个值能不能信路由参数解码一次还是两次这些问题在 Express 时代大多要自己查 RFC 自己判断写错一个就是漏洞。H3 的思路是把每个边界写进源码并在 JSDoc 里讲清楚理由readBody绝不从请求头自动探测 multipartassertBodySize拒绝同时携带Content-Length和Transfer-Encoding的请求getRouterParams解码后也永远不会还原出/。你不用先当半个安全专家才能安全地写一个 handler。30 秒跑起来import { H3, serve, readBody, assertBodySize } from h3; const app new H3(); // 轻量实例路由由 rou3 提供 app.get(/, () hello world); // 返回字符串即 200 text/plain app.post(/, async (event) { assertBodySize(event, 10 * 1024 * 1024); // 声明 10MB 上限读体超了抛 413 const data await readBody(event); // 默认按 JSON 解析 return { ok: true, data }; // 返回对象自动序列化为 JSON }); serve(app); // 绑定 app.fetch 到运行时服务器所有工具函数都从包入口导出清单见 src/index.ts。Node、Bun、Deno、Cloudflare 各有适配入口如 src/_entries/node.ts 里的serve与toNodeHandler同一份应用代码可直接换运行时。场景一接住请求——先验边界再取数据请求体超限怎么拦assertBodySize(event, limit)不是预缓冲而是把event.req换成 srvx 的limitRequestBody限流代理字节边读边计数累计超过limit的瞬间抛413。它还有两道前置防御src/utils/body.tsContent-Length诚实超标 → 处理前直接413不碰流同时带Content-Length与Transfer-Encoding→400这是请求走私向量RFC 7230。注意语义边界限流绑定消费chunked 请求的超限只在 handler 真正读体时触发从未读取的 body 不计入。想要中间件形式就用bodyLimit(limit)src/utils/middleware.ts语义完全相同。请求体怎么解析、何时校验readBody(event, options?)默认按 JSON 解析仅当Content-Type为application/x-www-form-urlencoded时回退 URL 编码。multipart/form-data必须显式传type: formData源码注释直接点明原因src/utils/body.ts// 反例依赖 Content-Type 自动嗅探 multipart // → 不可信请求可以把应用拖进昂贵的 multipart 解析 // 正例显式 opt-in解析失败抛 400 const body await readBody(event, { type: formData });解析后立即校验用readValidatedBody(event, validate, { onError? })validate可以是普通函数也可以是 Standard-Schema 兼容库zod、valibotonError可接收issues自定义statusText与汇总信息。查询参数同理getQuery(event)取解析对象嵌套解析用event.url.search拿原始串getValidatedQuery(event, schema)读完即校验src/utils/request.ts。场景二发出响应——流式输出与清理流式响应怎么发iterable(source)把生成器/异步迭代器变成流式响应chunk 限字符串或 Buffer。关键行为首个 chunk 在响应创建前被 await——也就是说第一个 chunk 之前你设置event.res.status和 headers 都还生效第一个 chunk 之后设置一律被忽略因为头已经上线路了src/utils/response.ts。生成器return的值与yield值同等对待。return iterable(async function* work() { yield ol; for (let i 0; i 10; i) { await delay(1000); yield lijob #${i} done/li; } return /ol; // return 值同样发出 });HTML 输出用标签模板html${userName}插值自动转义 已有转义标记的字符串用raw包起来绕过转义。**raw 只接受你信任的已转义片段把用户输入传进去等于重新引入 XSS。**重定向与资源清理redirect(location, status?)默认302响应体附带 meta refresh 页面兜底忽略头的老客户端。redirectBack(event, { fallback?, allowQuery? })基于referer返回上一页默认只取 pathname剥掉 query 与 hash跨源或缺失时落回fallback默认/。fallback必须是硬编码路径传用户输入就是开放重定向。onDispose(event, cb)注册事件彻底结束回调响应体流完、客户端断开或出错的任意一种都算结束按注册顺序在全局onResponse之后执行。注意它表示h3 处理完了不是客户端已收到要在产出响应过程中响应客户端断开比如中止上游 fetch监听event.req.signal。app.get(/task, (event) { const timer setInterval(tick, 1000); onDispose(event, () clearInterval(timer)); // 流结束/断开即清理 return iterable(stream); });场景三守住安全边界——路径、来源与会话路由参数怎么解码getRouterParams(event, { decode: true })只解码一层且编码的路径分隔符%2f、%5c及其任意%25嵌套永远保持编码src/utils/request.ts。这意味着// GET /files/%252e%252e/x getRouterParams(event); // { rest: %252e%252e/x } 原始形态 getRouterParams(event, { decode: true }); // { rest: %2e%2e/x } —— 到此为止 // 反例decodeURIComponent(params.rest) → ../x路径穿越 // 正例把返回值当最终值校验要落文件系统先过 resolveDotSegments解码一次的结果仍可能含%XX%252e%252e→%2e%2e二次decodeURIComponent会把它还原成../还顺带还原%00NUL 字节。路径穿越怎么防御resolveDotSegments(path, opts?)解析./..段且永远不会逃逸到根/之上结果恒为单一前导/的绝对路径杜绝//host协议相对形态。它按任意%25嵌套深度解码点段%2e、%252e…并把\归一为/所以%2e%2e/、..\..\与字面../一样被捕获src/utils/path.ts。尾部./..按目录处理并保留结尾斜杠RFC 3986 §5.2.4内部空段保留/a//b不变。两个可选参数面向下游差异decodeSlashes让范围检查也能捕获编码分隔符下游 nginx 会自己把%2f解回/mergeSlashes模拟合并空段的下游。配isCanonicalPath(path, opts?)做热路径快速守卫——文档说得很直接scope 检查里漏掉一次规范化是绕过漏洞而非性能问题。请求来源怎么判定getRequestIP(event)默认取event.req.ip连接对端或服务器配置信任代理后解析的地址。xForwardedFor: true改为取x-forwarded-for链的第一个条目——这正是客户端能自行写入的位置。// 反例getRequestIP(event, { xForwardedFor: true }) 用于限流/白名单 // → 任何调用者可自报 IP白名单、限流、审计全部击穿 // 正例让服务器侧信任代理srvx trustProxy 从右往左解析选项保持关闭getRequestHost/getRequestURL/getRequestProtocol同理x-forwarded-*选项一律 opt-in。由Host头推导的 origin 不能用于 CSRF/origin 校验、缓存键或发给其他用户的绝对链接除非上游已对 Host 做白名单固定。会话与 CORSuseSession(event, config)基于 iron 加密src/utils/session.ts、src/utils/internal/iron-crypto.ts会话写入加密签名后的 Cookie默认secure、httpOnly、sameSitelax、path/password必须至少 32 字符且要来自密码学随机源。maxAge是绝对寿命idleTimeout是滑动窗口通过重 seal Cookie 前移节流到每半个窗口一次。getSession/updateSession/clearSession与sealSession/unsealSession分别负责读写与加解密。handleCors(event, options)处理完整 CORS预检请求直接附加预检头并返回204返回值非false即已处理。origin默认*若传正则注意正则不锚定——/example\.com/会匹配https://example.com.evil.test必须写成/^https?:\/\/([a-z0-9-]\.)?example\.com$/这种带锚点转义的形态src/utils/cors.ts。场景四代理请求——内部子请求与外部转发三个函数层层递进src/utils/proxy.tsfetchWithEvent(event, url, init?)url以/开头 → 经event.app.fetch()派内部子请求继承过滤后的头与运行时元数据ip、waitUntil永不出进程外部 URL → 原生fetch(url, init)原样发送事件的头与上下文一律不继承向任意主机转发 Cookie/Authorization 不安全。proxyRequest(event, target, opts)把入站请求代理出去。请求体流式透传、不缓冲所以此前读过 bodyreadBody()、readFormData()或读体中间件会锁死流导致失败——需要先检查再代理就从event.req.clone()读app.all(/proxy, async (event) { const body await event.req.clone().json(); // 从克隆体检查 // ... return proxyRequest(event, target, { filterHeaders: [cookie, authorization], // 对不完全信任的上游剥离凭据 }); // 原请求流完好 });默认行为值得记住入站的Cookie与Authorization原样转发同信任的反向代理这是对的上游 3xx 默认透传不跟随xfwd默认false开启后也只把服务端视角的值追加/覆写进x-forwarded-*客户端值永远不赢。proxy(event, target, opts)与proxyRequest的关键差异是默认不转发入站头只发调用方通过opts.headers显式传的头且opts.filterHeaders对它无效。共同红线不要把未净化的用户输入直接当url/target——主机白名单、内部路径拦截、协议强制都是调用方的职责/开头的内部 target 会绕过外部安全层反向代理认证、IP 白名单、mTLS。场景五WebSocket 与 JSON-RPC 通道defineJsonRpcHandler({ methods, ... })生成实现 JSON-RPC 2.0 的 H3 处理器内置三条安全默认src/utils/json-rpc.ts强制 JSONContent-Type否则415不强制的话攻击者页面的 HTML 表单就构成 CORS simple request可带受害者 Cookie 免预检到达默认拒绝跨源请求带不匹配Origin的请求403无Origin的 CLI/服务端调用放行批处理默认上限 50 条每条并发派发不封顶等于一次请求扇出成任意次方法调用限流与配额全部失效。app.post(/rpc, defineJsonRpcHandler({ methods: { sum: ({ params }, event) (params as any).a (params as any).b, }, }));defineJsonRpcWebSocketHandler(methods, hooks?)走 WebSocket它不校验Origin——WebSocket 升级不受 CORS 约束任意源页面都能携带访客 Cookie 建连。请在upgrade钩子里自己校验Origin必要时抛Response中止。defineWebSocket(hooks)defineWebSocketHandler(http?)提供纯 WS 场景非升级请求默认426。原理深潜两个代表性实现为什么限流绑定消费而不是预缓冲assertBodySize的取舍见 src/utils/body.ts。预缓冲看似简单但会把整个 body 留在内存里、阻塞流式 handler而且拦不住撒谎偏小的Content-Length。H3 的做法是三步先处理诚实的超标声明处理前413再拒绝走私组合400最后把event.req替换成limitRequestBody包装的代理createError注入的是标准HTTPErrorbody 读取器与错误管线无需任何特殊映射就能把它当作已处理的413。代价是明确写出来的chunked/未知长度的超限只在 handler 读体时浮现而不是进 handler 前未被读取的 body 不计数。这是字节精确 零额外内存与入口拦截之间的选择H3 选了前者并在 JSDoc 里把两个后果都写明白。内部子请求如何堵死//evil.comproxy/fetchWithEvent用target[0] /判定内部目标但 URL 解析器会把更长的前导分隔符序列读成域名//evil.com/x甚至/\evil.com/x\对特殊协议也是分隔符。若照原样解析一个内部子请求会拿着外部 origin 出发。src/utils/proxy.ts 的createSubRequest用一条正则先折叠前导序列// 分隔符本身\ 也算 解析前会被剥掉的 C0 字符\t \n \r const LEADING_SEPARATOR_RUN_RE /^(?:[/\\]|[\t\n\r])/; const url new URL(path.replace(LEADING_SEPARATOR_RUN_RE, /), event.url);正则特意覆盖了 C0 字符因为/\t/evil.com会被 URL 解析器先剥掉制表符、再读成//evil.com——不堵这里就是绕过。解析时以event.url为基址内部 target 因此永远落在应用自己的 origin 上。坑与安全边界集中列几条最常见的误用每条都给正确写法代理前读了 bodyreadBody()之后再proxyRequest会因流被消费而失败。正确await event.req.clone().json()检查原流留给代理。路由参数二次解码decodeURIComponent再解一遍会把%2e%2e变回../。正确getRouterParams(event, { decode: true })的返回值当最终值落文件系统前过resolveDotSegments。信任x-forwarded-*第一个值任何xForwardedFor: true类配置都等于允许调用者自报 IP。正确服务器侧配置信任代理srvxtrustProxy读event.req.ip。开放重定向redirect(query.next)或redirectBack(event, { fallback: query.next })都是开放重定向。正确location/fallback走白名单或硬编码路径回上一页优先redirectBack的 pathname 默认行为。凭据外流向不完全信任的上游proxyRequest会原样转发Cookie与Authorization。正确filterHeaders: [cookie, authorization]或者改用proxy/fetchWithEvent它们默认就不继承事件头。raw接用户输入 / 正则 origin 不锚定前者重开 XSS后者让*.evil.test混进 CORS 白名单。正确raw只包可信标记CORS 正则^…$锚定并转义点号。生态与延伸文档导航索引docs/2.utils/0.index.md按 Request、Response、Cookie、Security、Proxy、MCP、More、Community 八类组织完整 API 清单即 src/index.ts 的 Utils 段。可运行示例examples/cookies.mjs、examples/cors.mjs、examples/query-params.mjs、examples/server-sent-events.mjs、examples/websocket.mjs、examples/nested-app.mjs。行为基准test/ 下与 utils 一一对应的用例如 test/cookies.test.ts、test/proxy.test.ts、test/security.test.ts、test/json-rpc.test.ts不确定某个默认行为时先看它们。社区兼容库Apitally、unjwt、Clear Router 等收录于 docs/2.utils/99.community.md。结语H3 的价值不在功能数量而在每个默认值都可追溯multipart 的 opt-in 解析、限流绑定消费、路由参数单层解码、代理凭据的转发策略、内部子请求的 origin 钉死——这些边界写在源码注释与 JSDoc 里而不是留给你踩。拿到new H3()这个轻量实例后按需取用工具函数就能用很少的代码跨 Node、Bun、Deno、Cloudflare 部署一个安全基线完整的服务。下一步建议通读 docs/2.utils/ 的八篇文档再对照test/里的用例把每个默认行为记牢。【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考