路由规则

像 CDN 或反向代理一样配置路由:请求头、重定向、CORS、缓存和代理,按 URL 模式声明。

重定向、缓存请求头、代理等,这些通常是在 CDN 或反向代理中配置的行为。路由规则将这一层引入你的 h3 应用:不必为每个关注点分别编写中间件,只需在一个声明式配置对象中描述一组路由应该发生什么

routeRules({
  "/old/**": { redirect: "/new/**" },
  "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
  "/api/**": { cors: true },
});

routeRules() 中间件会将每个请求与模式进行匹配,在路由处理程序运行前应用匹配的规则,并将合并后的结果暴露在 event.context.routeRules 上。

由于规则在应用内部运行,因此在 h3 支持的每个运行时上表现一致,也可以与前置的实际 CDN 组合使用。此外,由于规则是普通数据,框架和构建工具可以处理同一份配置,请参阅仅数据规则编译器

路由规则位于 h3/rules 子路径下。两个需要额外依赖的规则通过各自的子路径按需启用:缓存(h3/rules/cache)和代理(h3/rules/proxy);构建时的代码生成位于 h3/rules/compiler

快速开始

server.mjs
import { H3, serve } from "h3";
import { routeRules } from "h3/rules";
import { cache } from "h3/rules/cache"; // only needed for `cache` / `swr` rules (requires ocache)
import { proxy } from "h3/rules/proxy"; // only needed for `proxy` rules

const app = new H3();

app.use(
  routeRules(
    {
      "/blog/**": { swr: 60 },
      "/old/**": { redirect: { to: "/new/**", status: 301 } },
      "/api/proxy/**": { proxy: "https://example.com/**" },
      "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
      "/api/**": { cors: true },
      "GET /api/cached/**": { swr: 60 }, // applies to GET only
    },
    { handlers: { cache, proxy } },
  ),
);

serve(app);
Read more in Middleware.

内置规则

规则功能
headers设置响应请求头。
redirect发送服务端重定向。
cors使用 handleCors 处理 CORS。预检(OPTIONS)请求会直接得到响应。
cache缓存匹配的路由处理程序响应。按需启用,参阅缓存
swrcache: { swr: true, maxAge?: number } 的快捷方式。
proxy将请求转发到其他源或应用内路径。按需启用,参阅代理
这里特意没有 auth 规则。路由规则是声明式配置层;凭据检查应放在中间件中,以便针对你的用户存储进行验证,请使用 basicAuth 或你自己的中间件。如果你仍然希望将身份验证门禁作为规则使用,请编写自定义规则处理程序,并先阅读安全性(尤其是 restricting)。

headers

最终响应上设置请求头,也就是在 cacheredirectproxy 之后,因此这里设置的 cache-control 会覆盖缓存处理程序生成的值。

routeRules({
  "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
});

redirect

使用普通字符串进行 307 重定向,或使用 { to, status } 完全控制:

routeRules({
  "/old/**": { redirect: "/new/**" }, // /old/a?x=1 → /new/a?x=1 (307)
  "/search": { redirect: "https://example.com/s?lang=en" }, // /search?q=h3 → …/s?lang=en&q=h3
  "/legacy": { redirect: { to: "/", status: 301 } },
});

以下两种行为与 proxy 共享:

  • **通配符尾部:**当规则键和 to 都以 /** 结尾时,匹配到的尾部会追加到目标路径。
  • **转发查询参数:**请求的查询字符串会逐字转发(重复键和编码都会保留),并追加在目标中已有的查询参数之后。

cors

传入 true 使用宽松的默认值,或传入 CorsOptions 对象(源允许列表、credentialsmaxAge 等):

routeRules({
  "/api/**": { cors: { origin: ["https://example.com"], credentials: true } },
});
cors: true 会规范化为空选项对象({})。因此,在更具体的模式上,它会与从更宽泛模式继承的选项进行浅合并,而不是恢复宽松的默认值。使用 cors: false 可以完全重置继承的 CORS。

cacheswr

缓存匹配的路由处理程序响应。这些规则需要注册缓存处理程序,设置方式和详细信息请参阅缓存

swr: 60cache: { swr: true, maxAge: 60 } 的快捷方式。swr: 0 有效,而 swr: false 会重置继承的 cache 规则。

proxy

将匹配的请求转发到其他位置。此规则需要来自 h3/rules/proxy 的按需启用处理程序,参阅代理

匹配方式

模式使用 🌳 Rou3 进行匹配,该引擎与路由使用的引擎相同,匹配对象是 event.url.pathname

与只有最具体匹配项生效的路由处理程序不同,每个匹配的模式都会生效。匹配结果从最不具体到最具体进行合并:对象选项进行浅合并(更具体的键优先),其他值则整体覆盖。

routeRules({
  "/**": { headers: { "x-app": "demo" } },
  "/api/**": { headers: { "x-api": "1" } },
});

// GET /api/users → x-app: demo, x-api: 1

重置规则

在更具体的模式上将规则设置为 false,即可在该处移除规则:

routeRules({
  "/api/**": { cors: { origin: ["https://example.com"] } },
  "/api/public/**": { cors: false }, // no CORS handling under /api/public
});

按方法限定的规则

在键前加上 HTTP 方法即可限定其作用范围。不带方法的键适用于所有方法;按方法限定的规则会在(并可覆盖)不限定方法的规则之后合并。方法标记不区分大小写。

routeRules({
  "/api/**": { headers: { "x-api": "1" } }, // any method
  "GET /api/**": { swr: 60 }, // GET only
});

规则按照模式解析到的 route 分组,而不是按照文本分组:/users/*/users/:id/users/:userId 属于同一组,因此使用一种写法编写的不限定方法规则,仍会与使用另一种写法编写的按方法限定规则一起生效。

当同一路由的两种写法具有不同的具体程度(/a/*/a/:id)时,更通用的模式会最后解析并获胜,无论其中任何一个是否限定了方法。建议每条路由只使用一种写法。

读取匹配的规则

合并后的结果在处理程序和中间件中以 event.context.routeRules 的形式提供,并按规则名称作为键。每个条目都是合并后的规则选项,即你编写的内容,其中已合并更具体模式的选项,并展开了 swr 等快捷方式:

app.get("/blog/:slug", (event) => {
  const rules = event.context.routeRules;
  rules?.cache; // { swr: true, maxAge: 60 }
  rules?.redirect?.to; // "/new"
  rules?.headers?.["x-a"]; // "1"
});
默认情况下,匹配结果会被记忆化,因此上下文中的对象会在请求之间共享。请将它(以及其中保存的规则选项)视为只读

上下文不会说明规则来自哪个模式。如果你需要该来源信息(产生贡献的模式及其参数),请直接使用匹配器

const { routeRules, matchedRules } = matcher("GET", "/blog/post");
routeRules.cache; // { swr: true, maxAge: 60 } — same object the context gets
matchedRules.cache?.route; // "/blog/**"
matchedRules.cache?.params; // rou3 params of the contributing patterns

你可以注册多个 routeRules() 实例,它们会组合工作:每个实例都会将其匹配的规则合并到之前实例已经放入上下文的结果之上(后面的实例按规则名称覆盖前面的实例)。框架级规则集和应用级规则集可以共存。

仅数据规则

任何没有处理程序的键(prerenderisr 或你自己的键)都是仅数据规则:它会像其他规则一样参与匹配和合并,并且可以从 event.context.routeRules 中读取,但不会产生运行时行为。这对构建时工具以及自行处理配置的框架很有用。

routeRules({
  "/docs/**": { prerender: true },
});

// event.context.routeRules.prerender → true

要为仅数据键添加类型,请声明一次,参阅 TypeScript

缓存

H3 在 h3/rules不提供缓存实现cache 规则(以及 swr 快捷方式)需要注册 cache 处理程序,如果规则集使用了这些规则却没有处理程序,匹配器构造就会抛出异常。

现成的处理程序由 ocache 支持,位于 h3/rules/cache。ocache 是可选的对等依赖,请将其与 h3 一起安装。将处理程序放在独立子路径中,可以使不使用缓存的 bundle 完全不包含 ocache。

import { routeRules } from "h3/rules";
import { cache } from "h3/rules/cache";

// default: in-memory storage
app.use(routeRules({ "/blog/**": { swr: 60 } }, { handlers: { cache } }));

若要自定义存储或默认值,请创建处理程序实例:

import { createOcacheRuleHandler } from "h3/rules/cache";

app.use(
  routeRules(rules, {
    handlers: {
      cache: createOcacheRuleHandler({
        storage: myStorage, // ocache storage instance (or a factory), shared by every rule
        defaults: { staleMaxAge: 60 }, // ocache defaults incl. hooks (rule options win)
      }),
    },
  }),
);

条目如何生成键

cache 规则包装的是匹配的路由处理程序,因此只有在匹配到路由的位置才会生效。默认情况下,条目会存储在 "h3/route-rules" 组下,并使用 <handlerScope>:<method>:<rulePattern>:<matchedRoute> 名称:

  • 作用域对于每个处理程序实例以及每个匹配的路由处理程序都是唯一的,因此两个应用或两个匹配器永远不会读写彼此的条目,即使它们共享模块作用域的 cache 导出也一样。
  • 方法会使不带响应正文的 HEAD 响应与 GET 条目分离。

groupname 都是普通的 cache 规则选项,可以覆盖,但显式的 name 会替换整个默认值,包括隔离机制。

作用域在不同进程之间不稳定,因此持久化存储后端会在每个进程中重新填充,且不会在 worker 之间共享。传入 createOcacheRuleHandler({ id: "my-app" }) 可使用稳定的键,但仅应在该实例服务于单个应用时使用。

ocache 会为每个缓存处理程序提供独立的存储实例。相反,createOcacheRuleHandler 会让其所有规则指向同一个存储,即你传入的 storage,或者在第一次缓存请求时创建的内存存储,因此应用的路由共享一个有界缓存。使用相同 id 创建的两个实例也会共享该默认存储,因为它们的键已经匹配。

缓存处理程序能接收到什么

缓存处理程序只能看到缓存键所覆盖的请求数据,因此条目不可能依赖未包含在键中的内容:

  • 默认情况下会丢弃查询字符串:处理程序接收不带查询字符串的 URL,查询字符串也不会改变键。设置 allowQuery: ["page", "q"] 可允许指定名称,设置 allowQuery: true 可允许整个查询字符串。
  • 除非在 varies 中指定,否则会丢弃请求头(凭据规则请参阅凭据和 Cookie)。这包括条件请求头、链路追踪请求头和请求 ID 请求头,请在缓存规则之外的中间件中读取它们。
  • 当响应声明了键未覆盖的 Vary 名称、设置了 Cache-Control: no-storeprivateno-cache、状态码不是 200203301308,或者超过 maxBodySize 时,响应会被提供但不会存储

解析缓存条目的期限为 30 秒maxResolveTime);过期后所有等待者都会被拒绝,条目也会被移除。处理程序的 event.req.signal 会随之中止,因此处理程序自行执行上游 fetch 时应转发该 signal。

CookieAuthorizationProxy-Authorization 会从缓存处理程序看到的请求中移除。它们都不会改变自动生成的缓存键:如果不移除,一个根据 bearer token 渲染每个用户内容的处理程序就会将其缓存为匿名内容,随后提供给所有人,并向共享缓存公布 public, s-maxage=N
  • 设置 cache: { allowAuthorization: true } 可让凭据通过。凭据随后会参与缓存(哈希到键中,并合并到 Vary),因此每个凭据都会拥有自己的条目。
  • 如果运行时无法移除凭据请求头(请求头不可变且请求无法重建),缓存规则会以 500 失败请求,而不是使用不含凭据的键缓存带凭据的响应。
  • varies 中列出的请求头仍对缓存处理程序可见。由于它们属于键和响应的 Vary,每个值都会拥有自己的条目。列入 varies 的凭据是例外,仍会被移除;allowAuthorization 是唯一可以转发凭据的开关。
  • 处理程序的 Set-Cookie 只会发送给生成该响应的请求者,永远不会进入缓存条目,否则一个访问者的会话就会重放给所有人。allowCookies 仅作用于请求端:它指定哪些 cookie 片段会参与条目键生成并传递给处理程序。必须在每个响应中设置 cookie 的路由不应进行缓存。

Cache-Control 行为

  • 处理程序设置的、声明 privateno-storeCache-Control 会原样保留(ocache 也会拒绝存储此类响应)。
  • 处理程序设置的其他任何 Cache-Control 都会被规则生成的 public, max-age=N, s-maxage=N 替换。使用 headers 规则控制最终请求头。
  • 传入 sendCacheControl: false 可抑制生成的请求头。

使用你自己的缓存

若要接入其他缓存实现(完全不使用 ocache),请从核心工厂创建处理程序。defineCachedHandler 接收匹配的路由处理程序以及合并后的规则选项(其中已预填 group / name),并返回缓存包装器,这是为 Nitro 等框架提供集成的入口:

import { createCacheRuleHandler } from "h3/rules";

const cache = createCacheRuleHandler({
  defineCachedHandler: (handler, opts) => myCachedHandler(handler, opts),
});

声明式规则选项(RouteRuleConfig["cache"])是由 h3 所有、与 ocache 兼容的 CacheRuleOptions 模式。实现钩子(getKeyshouldCachegetMaxAge 等)不是规则数据,请通过处理程序工厂的 defaults 传入。

代理

与缓存一样,proxy 规则也是按需启用的:其处理程序导入 proxyRequest,因此位于 h3/rules/proxy 中,不使用代理的 bundle 不会包含它。请显式注册;如果规则集使用了 proxy 却没有处理程序,匹配器构造会抛出异常:

import { routeRules } from "h3/rules";
import { proxy } from "h3/rules/proxy";

app.use(
  routeRules({ "/api/proxy/**": { proxy: "https://example.com/**" } }, { handlers: { proxy } }),
);

目标遵循与 redirect 相同的规则:会追加 /** 尾部,并转发查询字符串。

如果希望让 cacheproxy 保持为仅数据规则(参与匹配并可从上下文读取,但没有运行时行为),请传入 handlers: { cache: undefined } / handlers: { proxy: undefined },而不是处理程序。

执行顺序

具有运行时行为的规则作为中间件运行,最外层优先:

cors (-3) → [-2 free] → headers (-1) → custom rules (0) → redirect (1) → proxy (2) → cache (3) → route handler

实际含义如下:

  • CORS 预检会在其他任何内容运行之前得到响应。
  • headers 会包装所有内部规则的响应(这就是它能够覆盖缓存生成的 cache-control 的原因)。
  • redirectproxycache 会在不调用下一个规则的情况下直接响应,因此各自拥有独立的顺序;其中最内层的 cache 会调度路由处理程序。
  • 由于 cache 会自行调度路由处理程序,它也会结束应用的全局中间件链,请参阅缓存路由和全局中间件
  • 默认顺序为 0 的自定义规则会在这三个终止规则之前运行,无论各规则使用什么模式编写。

order 是普通数字(数值越小越先运行),因此自定义处理程序可以位于任意位置。-2 特意留给需要在 headersredirectproxycache 之前短路的规则。共享同一顺序的规则会按规则名称顺序运行,结果是确定的但顺序本身没有特殊意义:能够短路的处理程序应设置显式顺序,而不要依赖这一点。顺序的含义请参阅安全性

选项

routeRules(config, options) 接受:

选项描述
baseURL为每个规则模式添加前缀(去除末尾斜杠)。
handlers按名称添加或覆盖规则处理程序。undefined 会使该规则变为仅数据规则。
memoizemethod + pathname 记忆化匹配结果。默认启用,参阅记忆化
preMerge在启动时解析每个模式的包含链,参阅预合并

自定义规则处理程序

规则处理程序的形式是 { handler, order? }

  • handler 根据匹配的规则构建 H3中间件
  • order 控制执行顺序(数值越小越先运行,默认为 0;内置规则占用 -3-113,参阅执行顺序)。

处理程序接收的匹配规则是一个包装器:{ options, route, params?, handler? },因此它可以看到合并后的 options 以及上下文中没有携带的来源信息(route 是产生贡献的最具体模式)。

app.use(
  routeRules(
    { "/x/**": { shout: "hello" } },
    {
      handlers: {
        shout: {
          handler: (matched) => (event) => {
            event.res.headers.set("x-shout", String(matched.options).toUpperCase());
          },
        },
      },
    },
  ),
);
限制访问的自定义处理程序(身份验证门禁、速率限制、IP 允许列表)还必须设置 restricting: true,原因请参阅安全性

性能

记忆化

对于给定的 method + pathname,合并结果完全确定,因此 routeRules() 默认会对其进行记忆化:重复请求会跳过规则查找、路径规范化、合并和中间件构造,热路径会变成一次 map 查找。

  • 默认条目上限为 1024,采用 FIFO 淘汰,因此无限的动态路径不会无限增长缓存。使用 memoize: { max } 调整。
  • 传入 memoize: false,即可每次从头解析(此时每个请求都会获得新的结果对象)。

对于更底层的匹配器,记忆化与构造过程相互独立:通过 memoizeRouteRulesMatcher(matcher, opts?) 包装任意匹配器即可按需启用,这样未记忆化的 bundle 可以通过 tree-shaking 将其移除。

预合并

preMerge: true 会预先解析每个模式的包含链(在匹配器启动时,或通过编译器在构建时解析),使每次请求只需解析最具体的匹配层,而不必合并所有匹配层。按方法限定和不限定方法的规则、false 重置以及每条规则的 params,都与默认的每次请求合并行为完全一致。

预合并要求规则集没有歧义。如果两个模式部分重叠(例如 /a/*/c/a/b/*,其中最具体的匹配不明确),或者使用无法分析的模式(正则参数):

  • 运行时匹配器会在启动时抛出异常。
  • **编译器具有故障安全机制:**它会记录警告并回退到普通编译,因此构建仍会生成正确的匹配器。

直接使用匹配器

routeRules() 是即插即用的入口。底层组件也会导出,供需要在中间件之外匹配规则的框架集成使用:

import { createRouteRulesMatcher, normalizeRouteRules, memoizeRouteRulesMatcher } from "h3/rules";
import { cache } from "h3/rules/cache";

const matcher = memoizeRouteRulesMatcher(
  createRouteRulesMatcher(normalizeRouteRules(config), {
    baseURL: "/base",
    preMerge: true,
    handlers: { cache },
  }),
);

const { routeRules, routeRuleMiddleware } = matcher("GET", "/blog/post");
  • normalizeRouteRules() 会展开快捷方式(swr)、规范化字符串和布尔形式,并规范化键。与 routeRules() 不同,createRouteRulesMatcher() 接收的是已规范化的规则,这使得不需要规范化的运行时 bundle 可以排除规范化代码。
  • 匹配结果为 { routeRules, matchedRules, routeRuleMiddleware }:合并后的选项 map(routeRules() 放入上下文的内容)、带有来源信息的同一组规则,以及有序的中间件链。
  • mergeMatchedRouteRules() 是纯合并步骤(输入匹配层,输出匹配规则),而 ruleHandlers 是默认处理程序注册表(headersredirectcors)。

TypeScript

规则的两端各有一个接口:RouteRuleConfig 描述你编写的内容RouteRules 描述合并后的解析结果。由于合并后的值就是配置值,自定义规则只需以同一种形式在两者中声明一次:

declare module "h3/rules" {
  interface RouteRuleConfig {
    /** Incremental Static Regeneration (handled at build time). */
    isr?: number | boolean;
    /** Add this route to the prerender queue. */
    prerender?: boolean;
    /** A data-only rule with no runtime handler. */
    audience?: "public" | "internal";
  }
  interface RouteRules {
    isr?: number | boolean;
    prerender?: boolean;
    audience?: "public" | "internal";
  }
}

// event.context.routeRules.audience → "public" | "internal" | undefined

要点如下:

  • RouteRuleConfig封闭的,未知键会产生编译错误,因此诸如 redirct 的拼写错误会在构建时被捕获。通过扩展接口可以重新开放并添加你的键。
  • RouteRules 为合并后的规则在所有出现位置提供类型:event.context.routeRules、匹配器的 routeRules,以及规则处理程序接收的 matchedRules(每条规则的 options)。
  • 仅数据规则在运行时会原样经过规范化和合并,扩展接口只影响类型。
  • RouteRules 是 h3 自己的接口,由 h3/rules 重新导出,因此 declare module "h3" 会合并到同一声明中,这也是 Nitro 和独立 h3-rules 包使用的写法。只有 RouteRuleConfig 专属于 h3/rules

重新声明内置规则

你也可以重新声明 h3 的内置键,而不仅仅是添加新键。拥有自身规则形状的框架可以重新定义 redirectproxy 等类型,此时该键会解析为扩展者的类型,而不是 h3 的类型:

declare module "h3" {
  interface RouteRules {
    redirect?: string | { to: string; status?: number };
    cors?: boolean;
  }
}

任何形状都可以接受,包括基本类型和 false 分支。RouteRules 不受约束;h3 的内置规则位于独立的 BuiltinRouteRules 中,只有在没有其他模块重新声明某个键时才会组合进去。上下文中的类型就是这个组合结果,以 ResolvedRouteRules 的名称导出。重新声明会替换 h3 的内置规则,而不是与其求交;交由 h3 管理的键会保留精确的选项类型,因此 rules.redirect?.to 无需缩小类型即可读取。

h3 只会组合自身的内置规则,并且不声明索引签名。如果在共享接口中添加一个宽泛的 [key: string]: unknown,其他模块的所有扩展都会变成类型错误,这也是未声明的仅数据键可以在运行时读取、但必须先声明后才有类型的原因。

另外还导出了两个供集成使用的类型:NormalizedRouteRules(调用 normalizeRouteRules() 后单个模式的规则,即 RouteRules 加上 false 重置和任意名称)以及 MatchedRouteRule(带有来源信息的合并规则,即规则处理程序接收的内容)。

安全性

路由规则经常用于限制或塑造敏感行为,因此匹配器默认采取多项预防措施。通常你不需要做任何事情,但如果编写了限制访问的自定义规则处理程序,请阅读本节。

编码和备用路径写法

规则会针对使用者可以解析的请求路径的每一种写法进行匹配,而不只是 h3 实际调度的写法,否则攻击者可以通过重新书写适用路径来绕过规则。

event.url.pathname 只会解码那些字符在 URL 序列化后仍能保留的转义(因此 /%40admin 已经作为 /@admin 提供),但其他字符仍保持编码状态:分隔符(%2f%5c)、任意嵌套深度的 %25、序列化器会重新添加的转义(%20、非 ASCII 字符)以及 C0 控制字符。另一方面,模式直接使用字符书写。因此,每个请求还会针对以下路径进行解析:

  • 规范形式(解码编码的分隔符,并解析 . / ..);
  • 合并斜杠形式(下游组件如 nginx 的 merge_slashes 所解析的形式);
  • 百分号解码形式
routeRules({
  "/@admin/**": { redirect: "/elsewhere" },
  "/a admin/**": { redirect: "/elsewhere" },
});

// GET /@admin/data     → matched
// GET /%40admin/data   → matched  (h3 serves it as /@admin/data)
// GET /a%20admin/data  → matched  (a proxied backend would serve it as "/a admin/data")
// GET /a%2520admin/x   → matched  (…and so would one that decodes twice)
// GET /admin%2fpanel   → matched against /admin/panel too

备用形式只能增加一条规则,或使用具体程度相同或更高的模式覆盖规则;通过精心构造的路径到达的更宽泛模式,永远不能降低已服务路径解析出的规则。

反向写法也会被规范化:配置时,写成 /a%20admin/** 的模式会被解码为 /a admin/**,因此不会只覆盖编码形式。规则键的转义解析方式与 h3 路由模式完全相同,因此转义的 rou3 元字符会变成该元字符:"/a/%3Aid":id 参数模式,"/f/%2A%2A" 是 catch-all 模式,并且与 app.get("/a/%3Aid") 匹配。只有 %2f%5c%25 会保持编码并按字面匹配,因为解码它们会改变模式的分段数量。

调度不受影响:路由仍使用已提供的 event.url.pathname,而 redirect / proxy 仍会转发原始路径字节。

重置和 restricting 标志

对于已服务路径使用 false 重置的规则,在备用形式之间需要特别注意。重置会作为删除操作应用,因此重置规则无法与从未匹配的规则区分,而更宽泛的备用形式可能会重新添加它。匹配器会根据规则的极性决定:

  • 允许某项行为的规则会保持重置。重新添加它只会放宽响应,因此例外优先:私有子树上的 cors: false 无法通过同一路径的精心构造写法撤销。所有内置规则(corsredirectheaderscacheproxy)都属于此类。
  • 限制某项行为的规则仍会被重新添加。限制采用失败关闭策略,因此由单段模式授予的例外不会跟随解码后比该模式覆盖更多分段的路径。

处理程序通过 RuleHandler.restricting 声明自身属于哪一类,默认值为 false。没有任何内置处理程序设置该值。

自定义限制型规则处理程序(身份验证门禁、速率限制、IP 允许列表)必须设置 restricting: true,否则某种写法上的 false 重置会在其他所有写法中将其豁免。默认值对于权限规则是安全的,但对于限制规则则是错误的,并且不会发出警告。完全没有处理程序的规则(例如由 Nitro 等使用者自行处理的仅数据规则)无法携带该标志;将其作为门禁使用的消费者必须在使用点自行进行判断。

还有一种朝向故障安全方向的残余情况:即使某个写法忠实地重新表达了授予权限的路径,权限重置也永远不会被恢复。{"/**": { cors }, "/app/*": { cors: false }}/app/a%2fb 上仍保持豁免,尽管其解码形式包含两个分段,严格来说可能超出了重置它的单段模式。所有出现错误的情况都会倾向于不应用权限

HEAD 和预检请求

HEAD 请求由匹配的 GET 路由提供服务(RFC 9110),因此 GET 限定的规则也适用于 HEAD,否则使用 HEAD 请求即可绕过以 GET /admin/** 为键的规则。显式的 HEAD 键仍会覆盖(并可重置)GET 规则。

CORS 预检以 OPTIONS 形式到达,因此,按浏览器在 Access-Control-Request-Method 中声明的方法限定的 cors 规则,也会为预检解析。该查找只采用 cors 规则,不会采用该方法限定的任何其他规则,因为浏览器发送预检时不带凭据,如果将门禁从预检中解除,所有预检都会被拒绝。

短路响应上的请求头

headers 位于顺序 -1,因此会包装所有在其内部运行的规则,但不会包装在其外部运行的规则。外部规则生成的响应(CORS 预检响应,或自由 -2 区间内的自定义处理程序响应)会在进入 headers 中间件之前短路,因此不会应用 headers 规则。其他所有响应都会经过它,包括更内部产生的错误响应(404 或抛出异常的处理程序)。

如果某个请求头也必须存在于此类响应中,请在 routeRules() 之前注册的全局中间件中设置它,并同时在 event.res.errHeaders 上设置;错误响应是根据该容器构建的,而不是根据 event.res.headers 构建的。

缓存路由和全局中间件

cache 规则会自行调度匹配的路由处理程序,而不是调用下一层,因此,routeRules() 之后注册的全局中间件永远不会对匹配到 cache 规则的路由运行,缓存未命中时同样如此:

app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
app.use(requireAuth); // never runs for /api/** — not even on the first request
app.get("/api/private/:id", handler);

请在所有必须对缓存路由运行的全局中间件之后注册 routeRules()

app.use(requireAuth); // runs first, for every request
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));

每条路由的中间件不受影响,它属于缓存规则调度的组合路由处理程序,因此会在缓存未命中时运行,并与响应一起被缓存(这也是不应将凭据检查放在那里的另一个原因)。redirectproxy 也会结束链,但它们会直接响应请求,永远不会到达路由处理程序,因此这种情况只会让 cache 显得出乎意料。

重定向和代理目标安全性

对于 /** 目标,会追加匹配到的尾部,并根据目标自身的基础路径检查最终路径;如果请求可能逃逸该基础路径(例如通过编码的 ..%2f 遍历),则会以 400 拒绝。

尾部通过移除规则键的前缀取得,并按分段计数,因此该键匹配的每个请求都必须具有相同的分段数量。能够匹配可变分段数量的前缀分段——catch-all(/a/**/old/**)、修饰符参数(/:lang?/old/**/x/:seg*/old/**)或跨越分隔符的分组(/x{/a}?/old/**)——没有固定计数,此类请求会以 400 拒绝,而不是使用错误截取的路径转发。普通参数、*、正则参数以及段内分组(/:lang/old/**/x/*/old/**/blog{-:title}?/old/**)均只匹配一个分段,表现符合预期。

CORS 凭据

credentials: true 要求显式提供 origin(允许列表或验证函数)。将其与通配符源组合会在启动时抛出异常,因为 Access-Control-Allow-Origin: * 对带凭据的请求无效。

构建时编译器

对于构建时代码生成,可以将规则集编译为 findRouteRules 函数,使 Rou3 不进入运行时 bundle:

import { compileRouteRules } from "h3/rules/compiler";

const mod = compileRouteRules(config, {
  preMerge: true, // optional: bake pre-merged chains into the generated matcher
});

mod.code; // whole module (also `String(mod)` / template interpolation)
// -> import { headers as __ruleHandlers__$headers } from "h3/rules";
// -> export const findRouteRules = (method, path) => ...;

compileRouteRules 返回拆分为两个可组合部分的模块:imports(处理程序的 import 语句)和 bodyexport const findRouteRules = … 声明),以及 code(整个模块,与 String(mod) 相同)。使用 code 写入独立模块,或提取 imports 并内联 body,将匹配器编织进更大的生成模块。

编译器入口会自行规范化输入(编译发生在构建时,因此该步骤没有额外负担),可以直接传入已编写的配置。已经规范化的规则集同样是有效输入,因为规范化具有幂等性。

在运行时,使用 createMatcherFromFind(findRouteRules) 包装 findRouteRules,并围绕它组合 memoizeRouteRulesMatcher 以实现记忆化。编译后的匹配器与运行时匹配器产生相同结果。

匹配器 API 接收的 method 必须已经转换为大写findRouteRulescreateMatcherFromFindcreateRouteRulesMatcher 都会按原样比较它。routeRules() 中间件会替你进行规范化;手写包装器必须传入 event.req.method.toUpperCase(),否则使用小写形式的请求将完全匹配不到按方法限定的规则,并跳过其门禁。
规则选项会嵌入为 JS 对象字面量,因此必须能够通过 JSON 往返转换。规则选项中的函数、DateRegExp(例如 cors.origin 验证函数)会导致编译失败并返回明确错误,而不会与运行时匹配器静默地产生差异。

具体程度保护

createMatcherFromFind 默认应用具体程度保护。当路径的某个备用形式(规范形式、合并斜杠形式、百分号解码形式)解析出的规则与已服务路径不同时,备用形式只能使用具体程度相同或更高的模式覆盖,因此宽泛的 /** 规则永远不会在精心构造的 %2f / %2e%2e 路径上降低较窄的 /admin/** 规则。

默认保护机制不依赖其他依赖,因此编译后的 bundle 不包含 Rou3。它根据模式形状判断包含关系,这是对精确关系的保守近似:只允许能够证明的包含关系,因此不会允许精确关系会拒绝的覆盖,但限制更严格。当无法判断时(命名 catch-all,例如 **:rest;正则或部分参数;或 :page? / :path* 这样的修饰符参数),它会保留已服务路径解析出的规则,而不应用较窄的规则。

使用 matcher: true,让编译器将精确关系写入生成的模块;当规则键使用修饰符参数时尤其推荐这样做。也可以将自定义谓词作为第二个参数传入,或传入 () => true 禁用保护。

按照具体程度对匹配层排序不会经过此谓词:它使用构建规则集时计算出的等级,因此没有内嵌关系的编译匹配器仍会像运行时匹配器一样解析相同的规则。

匹配器导出

若要跳过手写包装器,请传入 matcher,这样生成的模块会在 findRouteRules 旁边导出一个可直接使用的匹配器:

compileRouteRules(config, { matcher: true });
// -> export const findRouteRules = …;
// -> import { createMatcherFromFind } from "h3/rules";
// -> export const matcher = createMatcherFromFind(findRouteRules, /* baked specificity guard */);

// rename the export, or bake in memoization:
compileRouteRules(config, { matcher: { name: "routeMatcher", memoize: true } });
// -> import { createMatcherFromFind, memoizeRouteRulesMatcher } from "h3/rules";
// -> export const routeMatcher = memoizeRouteRulesMatcher(createMatcherFromFind(findRouteRules, …));

matcher: true 会将导出命名为 matcher;传入字符串可以重命名,或传入 { name?, memoize? } 以便同时使用 memoizeRouteRulesMatcher 包装(memoize: { max } 可调整上限)。仅当设置了 memoize 时才会导入 memoizeRouteRulesMatcher,因此未记忆化的匹配器导出仍可通过 tree-shaking 将其移除。基础设施导入会计入 mod.imports

处理程序来源

生成的模块只会导入规则集使用的规则处理程序。大多数内置规则都是 h3/rules 的具名导出(headersredirectcors),但按需启用的子路径处理程序除外:cache 来自 h3/rules/cacheproxy 来自 h3/rules/proxy,因此只有存在匹配规则时,它们的依赖才会进入 bundle。

每个处理程序的导入位置由 runtimeRules 控制。它是一个以规则名称为键的记录,值可以是模块 ID(该模块必须导出名称与规则键完全相同的成员),也可以是 { source, export }(导出名称不同时使用)。它会覆盖内置预设(DEFAULT_RUNTIME_RULES),因此只需列出新增或更改的内容。共享同一来源的处理程序会合并为一条 import 语句:

import { compileRouteRules } from "h3/rules/compiler";

compileRouteRules(config, {
  runtimeRules: {
    cache: "#nitro/cache", // repoint the built-in cache at your own module
    isr: { source: "#nitro/rules", export: "handleISR" }, // custom rule + export
  },
});
// -> import { handleISR as __ruleHandlers__$isr } from "#nitro/rules";
// -> import { cache as __ruleHandlers__$cache } from "#nitro/cache";
// (redirect, headers, … still import from "h3/rules" when used)