From 212b792871c49d8e5045d6c48be9249cf15e3800 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 3 Oct 2026 16:59:32 +0000 Subject: [PATCH] fix(vercel): skip ISR routes in observability routes Filtering in `getObservabilityRoutes` also drops their `config.json` route entries, which pointed to functions that are never created. --- src/presets/vercel/utils.ts | 18 ++--- test/unit/vercel-observability-routes.test.ts | 66 ++++++++++++++++++- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/presets/vercel/utils.ts b/src/presets/vercel/utils.ts index af050fd3c0..916f42a23c 100644 --- a/src/presets/vercel/utils.ts +++ b/src/presets/vercel/utils.ts @@ -175,15 +175,7 @@ export async function generateFunctionFiles(nitro: Nitro) { if (o11Routes.length === 0) { return; } - const routeRulesRouter = createPatternRouter(nitro.options.routeRules); for (const route of o11Routes) { - const routeRules = defu( - {}, - ...matchPattern(routeRulesRouter, route.route).reverse() - ) as NitroRouteRules; - if (routeRules.isr) { - continue; // #3563 - } const funcPrefix = resolve(nitro.options.output.serverDir, "..", route.dest); const funcDir = funcPrefix + ".func"; @@ -644,6 +636,12 @@ export function getObservabilityRoutes(nitro: Nitro): ObservabilityRoute[] { .map((route) => route.route.replace(SURROUNDING_SLASH_RE, "")) ); + // ISR routes are served by their ISR function, so they need neither an + // observability function nor a `config.json` route (#3563, #4447). + const routeRulesRouter = createPatternRouter(nitro.options.routeRules); + const hasISR = (route: string) => + (defu({}, ...matchPattern(routeRulesRouter, route).reverse()) as NitroRouteRules).isr; + const routePatterns = [ ...new Set([ ...(nitro.options.ssrRoutes || []), @@ -651,7 +649,9 @@ export function getObservabilityRoutes(nitro: Nitro): ObservabilityRoute[] { .filter((h) => !h.middleware && h.route) .map((h) => h.route!), ]), - ].filter((route) => !prerenderedPaths.has(route.replace(SURROUNDING_SLASH_RE, ""))); + ].filter( + (route) => !prerenderedPaths.has(route.replace(SURROUNDING_SLASH_RE, "")) && !hasISR(route) + ); return sortRoutes(routePatterns).map((route) => ({ route, diff --git a/test/unit/vercel-observability-routes.test.ts b/test/unit/vercel-observability-routes.test.ts index 9a38b1cde2..4c91014424 100644 --- a/test/unit/vercel-observability-routes.test.ts +++ b/test/unit/vercel-observability-routes.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { Nitro, NitroEventHandler, PrerenderRoute } from "nitro/types"; +import type { Nitro, NitroEventHandler, NitroRouteRules, PrerenderRoute } from "nitro/types"; import { getObservabilityRoutes } from "../../src/presets/vercel/utils.ts"; @@ -8,6 +8,7 @@ function createNitroStub(opts: { handlers?: NitroEventHandler[]; ssrRoutes?: string[]; prerenderedRoutes?: PrerenderRoute[]; + routeRules?: Record; }): Nitro { return { scannedHandlers: opts.handlers || [], @@ -16,6 +17,7 @@ function createNitroStub(opts: { compatibilityDate: { default: opts.compatibilityDate || "2025-07-15" }, handlers: [], ssrRoutes: opts.ssrRoutes || [], + routeRules: opts.routeRules || {}, }, } as unknown as Nitro; } @@ -155,4 +157,66 @@ describe("getObservabilityRoutes", () => { "foo", ]); }); + + // https://github.com/nitrojs/nitro/issues/4447 + describe("ISR route rules", () => { + it("skips static, dynamic and catch-all routes with an ISR rule", () => { + expect( + dests( + createNitroStub({ + handlers: [ + { route: "/schedule", handler: "schedule.ts" }, + { route: "/users/:id", handler: "user.ts" }, + { route: "/catchall/**", handler: "catchall.ts" }, + ], + routeRules: { + "/schedule": { isr: 300 }, + "/users/:id": { isr: 60 }, + "/catchall/**": { isr: 60 }, + }, + }) + ) + ).toEqual([]); + }); + + it("skips routes covered by a wildcard ISR rule", () => { + expect( + dests( + createNitroStub({ + handlers: [{ route: "/users/:id", handler: "user.ts" }], + routeRules: { "/users/**": { isr: 60 } }, + }) + ) + ).toEqual([]); + }); + + it("keeps routes only partly covered by an ISR rule", () => { + expect( + dests( + createNitroStub({ + handlers: [{ route: "/users/**", handler: "users.ts" }], + routeRules: { "/users/:id": { isr: 60 } }, + }) + ) + ).toEqual(["users/[...]"]); + }); + + it("keeps routes whose rules do not enable ISR", () => { + expect( + dests( + createNitroStub({ + handlers: [ + { route: "/users/:id", handler: "user.ts" }, + { route: "/plain", handler: "plain.ts" }, + ], + routeRules: { + "/users/**": { isr: 60 }, + "/users/:id": { isr: false }, + "/plain": { swr: true }, + }, + }) + ) + ).toEqual(["plain", "users/[id]"]); + }); + }); });