路由规则
Add headers, redirects, CORS, caching, and proxying to groups of routes with one configuration object.
Route rules let you configure behavior that often lives in a CDN or reverse proxy. Instead of writing separate middleware for each concern, describe what should happen for each URL pattern:
routeRules({
"/old/**": { redirect: "/new/**" },
"/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
"/api/**": { cors: true },
});Add routeRules() as global middleware. For each request, it:
event.context.routeRules.Tip
Route rules run inside your app, so they behave consistently across all runtimes supported by h3. You can still put a CDN in front of the app. Because the configuration is plain data, frameworks and build tools can also use it. See Data-Only Rules and the compiler.
Import the core feature from h3/rules. Caching and proxying have optional handlers in h3/rules/cache and h3/rules/proxy. Build-time code generation is available from h3/rules/compiler.
#快速开始
import { H3, serve } from "h3";
import { routeRules } from "h3/rules";
import { cache } from "h3/rules/cache"; // Needed for `cache` and `swr` (requires ocache)
import { proxy } from "h3/rules/proxy"; // Needed for `proxy`
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);#内置规则
Start with headers, redirect, or cors: these work without extra dependencies. Register the cache handler for cache and swr, or the proxy handler for proxy.
| Rule | What it does |
|---|---|
headers | Set response headers. |
redirect | Send a server-side redirect. |
cors | Handle CORS with handleCors. Preflight (OPTIONS) requests are answered directly. |
cache | Cache the matched route handler's response. Opt-in, see Caching. |
swr | Shortcut for cache: { swr: true, maxAge?: number }. |
proxy | Forward the request to another origin or an in-app path. Opt-in, see Proxying. |
Note
Route rules intentionally do not include an auth rule. Authentication needs executable logic that can check your user store, so it belongs in middleware. Use basicAuth or your own middleware. If you create an auth rule with a custom handler, read Security first, especially the restricting flag.
#headers
Sets headers on the final response. This happens after cache, redirect, and proxy, so a cache-control value here overrides one produced by the cache handler.
routeRules({
"/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
});#redirect
Pass a string to use the default 307 status. Pass { to, status } to choose the status:
routeRules({
"/old/**": { redirect: "/new/**" }, // /old/a?x=1 → /new/a?x=1 (307)
"/moved/**": { redirect: "/new?from=**" }, // /moved/a/b → /new?from=a/b
"/search": { redirect: "https://example.com/s?lang=en" }, // /search?q=h3 → …/s?lang=en&q=h3
"/legacy": { redirect: { to: "/", status: 301 } },
});Redirects and proxies share two useful behaviors:
- Wildcard tails: When the pattern ends in
/**, a**in the target is replaced with the matched part of the path. A trailingto: "/new/**"appends it; a**anywhere else in the target's path, query, or fragment interpolates it in place (/new/**/edit,/new?from=**). An empty tail — a request to exactly the pattern's base — substitutes an empty string. In a query or fragment value the tail is percent-encoded so it cannot add parameters; in a path position it is forwarded byte for byte. The tail can never change the target's origin: a**that could name the destination host ("**","**.cdn.example/x","https://**.example.com") is rejected at startup, and a request whose tail would still move the origin gets a400. A target**is left literal when no/**pattern applies to the route. - Query forwarding: h3 preserves the request query string, including duplicate keys and encoding. If the target already has a query, the request query is appended to it.
#cors
Pass true to use permissive defaults. Pass a CorsOptions object to configure an origin allowlist, credentials, maxAge, and other options:
routeRules({
"/api/**": { cors: { origin: ["https://example.com"], credentials: true } },
});Note
Internally, cors: true becomes an empty options object ({}). On a more specific pattern, it inherits options from broader matching patterns instead of resetting them to permissive defaults. Use cors: false to remove inherited CORS behavior.
#cache 和 swr
Caches the response from the matched route handler. You must register a cache handler first. See Caching.
swr: 60 is shorthand for cache: { swr: true, maxAge: 60 }. A value of 0 is valid. Use swr: false to remove an inherited cache rule.
#proxy
将匹配的请求转发到其他位置。此规则需要来自 h3/rules/proxy 的按需启用处理程序,参阅代理。
#匹配方式
Route rules use 🌳 Rou3, the same engine as routing. Patterns are matched against event.url.pathname.
Route matching and rule matching differ in one important way: a route uses only its most specific match, but route rules apply every matching pattern. H3 merges matches from least specific to most specific. Object options are shallow-merged, with the more specific values winning. Primitive values and other non-object values are replaced completely.
routeRules({
"/**": { headers: { "x-app": "demo" } },
"/api/**": { headers: { "x-api": "1" } },
});
// GET /api/users → x-app: demo, x-api: 1#重置规则
Set a rule to false on a more specific pattern to turn off inherited behavior for that part of your app:
routeRules({
"/api/**": { cors: { origin: ["https://example.com"] } },
"/api/public/**": { cors: false }, // no CORS handling under /api/public
});#按方法限定的规则
Add an HTTP method before a pattern when a rule should apply only to that method. Patterns without a method apply to every method. Method-specific rules merge last, so they can override general rules. Method names are case-insensitive.
routeRules({
"/api/**": { headers: { "x-api": "1" } }, // any method
"GET /api/**": { swr: 60 }, // GET only
});H3 groups equivalent route patterns together. For example, /users/*, /users/:id, and /users/:userId describe the same route group. A general rule using one spelling can therefore merge with a method-specific rule using another spelling.
Note
If equivalent spellings have different specificity, such as /a/* and /a/:id, the more general pattern resolves last and wins. This applies whether or not the rules are method-scoped. To avoid surprising results, use one spelling consistently.
#读取匹配的规则
Handlers and middleware can read the final merged configuration from event.context.routeRules. Entries are keyed by rule name. More specific values are already merged, and shortcuts such as swr are already expanded:
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"
});Important
Match results are memoized by default. This means the same result object can be shared by multiple requests. Always treat event.context.routeRules and its nested values as read-only.
The context contains the merged values, but not the patterns they came from. Framework integrations that need the contributing pattern and its parameters can use a matcher directly:
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 patternsYou can register routeRules() more than once. Each instance merges its results over earlier instances, and the later instance wins for the same rule name. This lets a framework provide defaults while an app adds or overrides its own rules.
#仅数据规则
A rule without a registered handler is data-only. It is still matched, merged, and exposed through event.context.routeRules, but it does not change the response at runtime. Frameworks and build tools can use data-only keys such as prerender, isr, or custom metadata.
routeRules({
"/docs/**": { prerender: true },
});
// event.context.routeRules.prerender → trueDeclare custom data-only keys to make them type-safe. See TypeScript.
#缓存
The core h3/rules package does not include a cache implementation. To use cache or swr, register a cache handler. H3 throws while creating the matcher if these rules are present without a handler.
H3 provides an optional handler backed by ocache in h3/rules/cache. Install ocache alongside h3; it is an optional peer dependency. Apps that do not use caching will not include ocache in their bundles.
import { routeRules } from "h3/rules";
import { cache } from "h3/rules/cache";
// default: in-memory storage
app.use(routeRules({ "/blog/**": { swr: 60 } }, { handlers: { cache } }));The default handler uses in-memory storage. Create your own handler instance to change the storage or default options:
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)
}),
},
}),
);#条目如何生成键
The cache rule wraps the matched route handler, so it only runs when h3 finds a route. By default, entries use the "h3/route-rules" group and the name <handlerScope>:<method>:<rulePattern>:<matchedRoute>:
- The scope is unique to both the handler instance and the matched route handler. Two apps or matchers cannot read or write each other's entries, even if they share the module-level
cacheexport. - The method prevents a body-less
HEADresponse from being stored as theGETresponse.
You can override group and name as normal cache rule options. Be careful: an explicit name replaces the entire default name, including its isolation.
The generated scope changes between processes. With persistent storage, each process therefore creates its own entries, and workers do not share them. For stable keys, pass createOcacheRuleHandler({ id: "my-app" }), but only use that instance for one app.
Note
Normally, ocache gives each cached handler its own storage instance. createOcacheRuleHandler instead shares one store across all of its rules. This is either the storage you provide or a memory store created on the first cached request. The result is one bounded cache for all routes in the app. Two instances with the same id also share that default store because their keys match.
#缓存处理程序能接收到什么
H3 only passes request data to the cached handler when that data is represented in the cache key. This prevents cached responses from depending on values that do not vary the entry:
- Query strings are removed by default. The handler receives a URL without a query, and the query does not affect the key. Use
allowQuery: ["page", "q"]to allow specific names, orallowQuery: trueto include the full query string. - Headers are removed unless listed in
varies. See Credentials and Cookies for additional credential rules. Conditional, tracing, and request-ID headers are also removed; read them in middleware outside the cache rule. - A response is returned but not stored if it uses a
Varyheader that the key does not cover, setsCache-Control: no-store,private, orno-cache, has a status other than200,203,301, or308, or is larger thanmaxBodySize.
Resolving an entry has a 30-second deadline, controlled by maxResolveTime. When the deadline expires, all waiters are rejected and the entry is evicted. The handler's event.req.signal is aborted too. Forward this signal if the handler makes an upstream fetch request.
#凭据和 Cookie
Important
H3 removes Cookie, Authorization, and Proxy-Authorization before calling a cached handler. These headers do not vary the automatically generated key. Without this protection, a response rendered for one user could be cached under an anonymous key, served to other users, and marked public, s-maxage=N for shared caches.
- Set
cache: { allowAuthorization: true }to pass the authorization credential to the handler. H3 hashes it into the key and adds it toVary, so each credential receives a separate entry. - Some runtimes may provide immutable headers and a request that cannot be rebuilt. If h3 cannot remove credentials safely, it returns a
500instead of caching a credentialed response under a credential-free key. - Headers listed in
variesremain visible to the cached handler. Each value gets its own key and is added to the response'sVaryheader. Credentials are an exception: listing them invariesdoes not forward them. UseallowAuthorizationinstead. - A handler's
Set-Cookieheader is sent only to the request that produced it and is never stored in the cache. This prevents one visitor's session cookie from being replayed to others.allowCookiesonly controls the request: it selects which cookie values reach the handler and vary the entry. Do not cache a route that must set a cookie on every response.
#Cache-Control 行为
- H3 preserves a handler's
Cache-Controlheader when it containsprivateorno-store. Ocache also refuses to store the response. - H3 replaces any other handler-provided
Cache-Controlvalue with the rule's generatedpublic, max-age=N, s-maxage=N. Use aheadersrule when you need full control over the final value. - Set
sendCacheControl: falseto disable the generated header.
#使用你自己的缓存
You can use another cache implementation instead of ocache. Create a handler with the core factory. defineCachedHandler receives the matched route handler and merged rule options, with group and name already filled in, and returns a cached wrapper. Frameworks such as Nitro can integrate here:
import { createCacheRuleHandler } from "h3/rules";
const cache = createCacheRuleHandler({
defineCachedHandler: (handler, opts) => myCachedHandler(handler, opts),
});The declarative options in RouteRuleConfig["cache"] use h3's ocache-compatible CacheRuleOptions schema. Implementation hooks such as getKey, shouldCache, and getMaxAge are not rule data. Pass them through the handler factory's defaults instead.
#代理
Proxying is also opt-in. The handler uses proxyRequest, so it is exported separately from h3/rules/proxy. Apps that do not proxy will not include it in their bundles. Register the handler explicitly; h3 throws while creating the matcher if a proxy rule has no handler:
import { routeRules } from "h3/rules";
import { proxy } from "h3/rules/proxy";
app.use(
routeRules({ "/api/proxy/**": { proxy: "https://example.com/**" } }, { handlers: { proxy } }),
);Proxy targets behave like redirect targets: h3 substitutes matching /** tails and forwards the query string.
Tip
You can keep cache or proxy as data-only rules. Pass handlers: { cache: undefined } or handlers: { proxy: undefined }. The rule will still be matched and exposed on the context, but it will not run any behavior.
#执行顺序
Rules that have runtime handlers run as middleware. Lower order numbers run first and wrap the rules inside them:
cors (-3) → [-2 free] → headers (-1) → custom rules (0) → redirect (1) → proxy (2) → cache (3) → route handlerIn practice:
- CORS can answer a preflight request before any other rule runs.
headerswraps every rule inside it, so its values override headers produced by caching or other inner rules.redirect,proxy, andcachecan finish the request without calling the next rule. Each has a separate order.cacheis innermost and dispatches the route handler.- Because
cachedispatches the route itself, it also ends the app's global middleware chain for every request it can cache. See Cached Routes and Global Middleware. - Custom rules use order
0by default, so they run beforeredirect,proxy, andcache.
order is a number, and lower values run first. The -2 slot is intentionally free for a custom rule that must short-circuit before headers, redirect, proxy, and cache. Rules with the same order run by rule name. That order is deterministic but has no semantic meaning, so give any short-circuiting handler an explicit order. See Security for the security implications.
#选项
Pass these options as the second argument to routeRules(config, options):
| 选项 | 描述 |
|---|---|
baseURL | 为每个规则模式添加前缀(去除末尾斜杠)。 |
handlers | 按名称添加或覆盖规则处理程序。undefined 会使该规则变为仅数据规则。 |
memoize | 按 method + pathname 记忆化匹配结果。默认启用,参阅记忆化。 |
preMerge | 在启动时解析每个模式的包含链,参阅预合并。 |
#自定义规则处理程序
Use a custom handler when you need runtime behavior that is not built in. A handler definition has the shape { handler, order? }:
handlerturns a matched rule into H3 middleware.ordercontrols when it runs. Lower values run first and the default is0. Built-in rules use-3through-1and1through3; see Execution Order.
The handler receives { options, route, params?, handler? }. options contains the merged value. route is the most specific contributing pattern, which is provenance not available on event.context.routeRules.
app.use(
routeRules(
{ "/x/**": { shout: "hello" } },
{
handlers: {
shout: {
handler: (matched) => (event) => {
event.res.headers.set("x-shout", String(matched.options).toUpperCase());
},
},
},
},
),
);Important
会限制访问的自定义处理程序(身份验证门禁、速率限制、IP 允许列表)还必须设置 restricting: true,原因请参阅安全性。
#性能
#记忆化
A given method + pathname always produces the same merged result, so routeRules() memoizes results by default. Repeated requests can skip pattern lookup, path canonicalization, merging, and middleware construction, reducing the hot path to a map lookup.
- The memoization map holds up to
1024entries by default. Dynamic paths therefore cannot grow it without limit. Change the cap withmemoize: { max }. - Eviction uses SIEVE: entries are evicted in insertion order, except that an entry requested since the eviction hand last passed it survives that pass. A small set of hot paths is therefore not displaced by a flood of one-shot dynamic paths, which plain FIFO would evict it alongside. A cache hit stays a single map lookup — unlike LRU, nothing is reordered on read.
- Use
memoize: falseto resolve every request again and create fresh result objects.
Lower-level matchers do not enable memoization automatically. Wrap a matcher with memoizeRouteRulesMatcher(matcher, opts?) to opt in. If you do not use it, bundlers can tree-shake the memoization code.
#预合并
Set preMerge: true to merge each pattern's inheritance chain ahead of time, either when the matcher starts or at build time with the compiler. Each request then resolves only the most specific layer instead of merging every matched layer. Method-specific rules, general rules, false resets, and per-rule params behave the same as they do with normal per-request merging.
Pre-merging only works for rule sets whose relationships can be determined in advance. It cannot safely analyze partial overlaps such as /a/*/c and /a/b/*, where the most specific match is ambiguous, or patterns with regex parameters:
- The runtime matcher throws during startup.
- The compiler falls back safely. It logs a warning and uses normal compilation, so the generated matcher remains correct.
#直接使用匹配器
Most apps should use routeRules(). Frameworks that need to resolve rules outside middleware can use the lower-level exports directly:
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()expands shortcuts such asswr, normalizes string and boolean forms, and canonicalizes keys.createRouteRulesMatcher()expects these normalized rules, unlikerouteRules(). This keeps normalization code out of runtime bundles that do not need it.- A match returns
{ routeRules, matchedRules, routeRuleMiddleware }: the merged values placed on the event context, those values with pattern provenance, and the ordered middleware chain. mergeMatchedRouteRules()is the pure merge operation: it accepts matched layers and returns matched rules.ruleHandlersis the default registry forheaders,redirect, andcors.
#TypeScript
Two interfaces describe route rules. RouteRuleConfig types the configuration you write, while RouteRules types the values produced by matching and merging. Declare a custom rule with the same shape in both interfaces:
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要点如下:
RouteRuleConfigis closed. Unknown keys cause type errors, so TypeScript catches a typo such asredirct. Module augmentation adds your custom keys.RouteRulestypes merged values everywhere they appear:event.context.routeRules, therouteRulesreturned by a matcher, and each matched rule'soptionspassed to a handler.- Data-only rules pass through normalization and merging unchanged. Module augmentation affects only their types.
- H3 owns the
RouteRulesinterface and re-exports it fromh3/rules. Augmentingdeclare module "h3"therefore updates the same declaration used by Nitro and the standaloneh3-rulespackage.RouteRuleConfigexists only inh3/rules.
#重新声明内置规则
Frameworks can also redeclare a built-in key when they provide a different rule shape. The augmented type replaces h3's type for that key:
declare module "h3" {
interface RouteRules {
redirect?: string | { to: string; status?: number };
cors?: boolean;
}
}The replacement can use any shape, including primitives and false. RouteRules is unconstrained. Built-in definitions live separately in BuiltinRouteRules and are added only for keys that have not been redeclared. This combined context type is exported as ResolvedRouteRules. A redeclaration replaces the built-in type instead of intersecting with it. Built-in keys you do not redeclare keep their exact option types, so expressions such as rules.redirect?.to do not require extra narrowing.
Note
h3 只会组合自身的内置规则,并且不声明索引签名。如果在共享接口中添加一个宽泛的 [key: string]: unknown,其他模块的所有扩展都会变成类型错误,这也是未声明的仅数据键可以在运行时读取、但必须先声明后才有类型的原因。
另外还导出了两个供集成使用的类型:NormalizedRouteRules(调用 normalizeRouteRules() 后单个模式的规则,即 RouteRules 加上 false 重置和任意名称)以及 MatchedRouteRule(带有来源信息的合并规则,即规则处理程序接收的内容)。
#安全性
The matcher protects against several path and ordering edge cases by default. Most apps do not need extra configuration. Read this section carefully if a custom rule restricts access.
#编码和备用路径写法
H3 matches rules against every meaningful interpretation of the request path, not only the spelling used for route dispatch. Otherwise, an attacker could bypass a rule by encoding the same path differently.
event.url.pathname decodes an escape only when the decoded character survives URL serialization. For example, /%40admin is already served as /@admin. Other values remain encoded, including separators (%2f, %5c), %25 at any nesting depth, values the serializer would encode again (%20 and non-ASCII characters), and C0 controls.
Route patterns, however, are normally written with the character itself. To prevent encoded paths from bypassing those patterns, h3 also resolves each request against:
- 规范形式(解码编码的分隔符,并解析
./..); - 合并斜杠形式(下游组件如 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 tooAn alternate interpretation can add a rule or override it only with an equally or more specific pattern. A crafted path can never use a broader pattern to weaken the rule selected for the served path.
H3 also normalizes encoded rule patterns at configuration time. For example, /a%20admin/** becomes /a admin/**, so the pattern does not cover only the encoded spelling.
Rule keys decode escapes in the same way as h3 route patterns. An encoded Rou3 metacharacter therefore becomes a metacharacter: "/a/%3Aid" is the :id parameter pattern, and "/f/%2A%2A" is a catch-all. This matches the behavior of app.get("/a/%3Aid"). Only %2f, %5c, and %25 remain encoded and match literally, because decoding them would change the number of path segments.
调度不受影响:路由仍使用已提供的 event.url.pathname,而 redirect / proxy 仍会转发原始路径字节。
#重置和 restricting 标志
A false reset needs special handling across alternate path interpretations. Because a reset removes a rule, it would otherwise look the same as a rule that never matched, allowing a broader alternate interpretation to add it again. The matcher handles this based on whether the rule permits or restricts behavior:
- A rule that permits behavior stays reset. Restoring it could loosen the response, so the exemption wins. For example, a crafted path cannot undo
cors: falseon a private subtree. All built-in rules (cors,redirect,headers,cache, andproxy) belong to this category. - A rule that restricts behavior is added again. This fail-closed approach prevents an exemption for a single-segment pattern from carrying over to a decoded path with more segments.
处理程序通过 RuleHandler.restricting 声明自身属于哪一类,默认值为 false。没有任何内置处理程序设置该值。
Important
自定义限制型规则处理程序(身份验证门禁、速率限制、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 构建的。
#缓存路由和全局中间件
The cache rule dispatches the matched route handler itself instead of calling the next layer, so global middleware registered after routeRules() never runs for a cacheable request to a route a cache rule matched — on a cache miss just as much as on a hit:
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
app.use(requireAuth); // never runs for a cacheable /api/** request — not even the first
app.get("/api/private/:id", handler);请在所有必须对缓存路由运行的全局中间件之后注册 routeRules():
app.use(requireAuth); // runs first, for every request
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));每条路由的中间件不受影响,它属于缓存规则调度的组合路由处理程序,因此会在缓存未命中时运行,并与响应一起被缓存(这也是不应将凭据检查放在那里的另一个原因)。redirect 和 proxy 也会结束链,但它们会直接响应请求,永远不会到达路由处理程序,因此这种情况只会让 cache 显得出乎意料。
Requests the cache never serves are the exception. The ocache handler passes anything it would not store — every method other than GET and HEAD, and any request carrying a Range header — down the chain instead, the way a CDN sends an uncacheable request to its origin. A POST to a cache-matched route therefore runs the middleware registered after routeRules(), reaches the route through normal dispatch, and keeps its Authorization header:
app.use(routeRules({ "/api/**": { swr: 60 } }, { handlers: { cache } }));
app.use(requireAuth); // skipped for a cacheable GET — but runs for POST, PUT, ...
app.get("/api/non-cachable/:id", handler);
app.post("/api/non-cachable/:id", handler);A custom cache handler declares its own uncacheable requests with the shouldBypass option of createCacheRuleHandler; without it, every request to a matched route ends the chain. A shouldBypassCache hook passed through createOcacheRuleHandler({ defaults }) is resolved inside the cache instead, so it does not pass the request through.
That same composition is why routeRules() belongs in app.use(), not on a route:
// Works, but the whole rule chain runs twice per request:
app.get("/api/x", handler, { middleware: [routeRules(rules, { handlers: { cache } })] });Registered this way (or composed in with defineHandler({ middleware })), the rule sits inside the very handler the cache rule dispatches, so the dispatch re-enters it. The rule detects the re-entry and continues to the route handler instead of dispatching a second time — the response and the caching are correct — but every other matched rule still runs on both passes. Keep routeRules() global.
#Redirect and Proxy Target Safety
对于 /** 目标,会追加匹配到的尾部,并根据目标自身的基础路径检查最终路径;如果请求可能逃逸该基础路径(例如通过编码的 ..%2f 遍历),则会以 400 拒绝。
H3 builds the tail by removing the rule pattern's prefix, counted in segments. That prefix must therefore contain the same number of segments for every matching request.
Some patterns can match a variable number of prefix segments. These include a catch-all (/a/**/old/**), a modifier parameter (/:lang?/old/**, /x/:seg*/old/**), or a group that spans a separator (/x{/a}?/old/**). H3 rejects these requests with 400 rather than forwarding a path with the wrong prefix removed.
Plain parameters, *, regex parameters, and groups within one segment (/:lang/old/**, /x/*/old/**, /blog{-:title}?/old/**) each match exactly one segment and work as expected.
#CORS 凭据
credentials: true 要求显式提供 origin(允许列表或验证函数)。将其与通配符源组合会在启动时抛出异常,因为 Access-Control-Allow-Origin: * 对带凭据的请求无效。
#构建时编译器
Framework and build-tool authors can compile a rule set into findRouteRules. This keeps Rou3 out of the runtime 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 returns three forms:
imports: handler import statements.body: theexport const findRouteRules = …declaration.code: the complete module, also returned byString(mod).
Write code as a standalone module, or combine imports and body with a larger generated module.
Compiler entry points normalize their input automatically, so you can pass authored configuration directly. Already-normalized rules also work because normalization is idempotent.
At runtime, turn findRouteRules into a matcher with createMatcherFromFind(findRouteRules). Wrap that matcher with memoizeRouteRulesMatcher to enable memoization. Compiled and runtime matchers return the same results.
Important
匹配器 API 接收的 method 必须已经转换为大写:findRouteRules、createMatcherFromFind 和 createRouteRulesMatcher 都会按原样比较它。routeRules() 中间件会替你进行规范化;手写包装器必须传入 event.req.method.toUpperCase(),否则使用小写形式的请求将完全匹配不到按方法限定的规则,并跳过其门禁。
Note
规则选项会嵌入为 JS 对象字面量,因此必须能够通过 JSON 往返转换。规则选项中的函数、Date 或 RegExp(例如 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 的具名导出(headers、redirect、cors),但按需启用的子路径处理程序除外:cache 来自 h3/rules/cache,proxy 来自 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)