From 3214724f569b282f12a857759f7b73fa10b94460 Mon Sep 17 00:00:00 2001 From: Florian Streise Date: Fri, 6 Mar 2026 19:45:44 +0100 Subject: [PATCH 1/3] feat(cache): add `allowQuery` option to filter query params in cache key Allow users to specify which query parameters should be included in the cache key via `allowQuery` option on `defineCachedEventHandler` and route rules cache config. - `undefined`: all query params included (default, no behavior change) - `[]`: all query params ignored (only pathname used) - `["q"]`: only listed params included Closes nuxt/nuxt#33728 Co-Authored-By: Claude Opus 4.6 --- docs/1.guide/6.cache.md | 4 ++++ src/runtime/internal/cache.ts | 18 +++++++++++++++++- src/types/runtime/cache.ts | 7 +++++++ test/fixture/api/cached-allow-query.ts | 8 ++++++++ test/fixture/nitro.config.ts | 3 +++ test/tests.ts | 21 +++++++++++++++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/fixture/api/cached-allow-query.ts diff --git a/docs/1.guide/6.cache.md b/docs/1.guide/6.cache.md index d7fdde13f3..b0b86a9baf 100644 --- a/docs/1.guide/6.cache.md +++ b/docs/1.guide/6.cache.md @@ -267,6 +267,10 @@ The `cachedEventHandler` and `cachedFunction` functions accept the following opt ::field{name="varies" type="string[]"} An array of request headers to be considered for the cache, [learn more](https://github.com/nitrojs/nitro/issues/1031). If utilizing in a multi-tenant environment, you may want to pass `['host', 'x-forwarded-host']` to ensure these headers are not discarded and that the cache is unique per tenant. :: + ::field{name="allowQuery" type="string[]"} + List of query parameter names to include in the cache key. If `undefined`, all query parameters are included. If an empty array `[]`, all query parameters are ignored (only the pathname is used for caching). Only applicable to cached event handlers. :br + Defaults to `undefined`. + :: :: ## Cache keys and invalidation diff --git a/src/runtime/internal/cache.ts b/src/runtime/internal/cache.ts index f848b82c79..328c905f37 100644 --- a/src/runtime/internal/cache.ts +++ b/src/runtime/internal/cache.ts @@ -237,8 +237,24 @@ export function defineCachedEventHandler< return escapeKey(customKey); } // Auto-generated key - const _path = + const _rawPath = event.node.req.originalUrl || event.node.req.url || event.path; + let _path: string; + if (opts.allowQuery) { + const parsed = parseURL(_rawPath); + const params = new URLSearchParams(parsed.search); + const filtered = new URLSearchParams(); + for (const key of opts.allowQuery) { + const value = params.get(key); + if (value !== null) { + filtered.set(key, value); + } + } + const search = filtered.size > 0 ? `?${filtered.toString()}` : ""; + _path = parsed.pathname + search; + } else { + _path = _rawPath; + } let _pathname: string; try { _pathname = diff --git a/src/types/runtime/cache.ts b/src/types/runtime/cache.ts index 7e0d6ce53c..239f0e22c2 100644 --- a/src/types/runtime/cache.ts +++ b/src/types/runtime/cache.ts @@ -37,4 +37,11 @@ export interface CachedEventHandlerOptions extends Omit< > { headersOnly?: boolean; varies?: string[] | readonly string[]; + /** + * List of query string parameter names that will be considered for caching. + * - If undefined, all query parameters are included in the cache key. + * - If an empty array `[]`, all query parameters are ignored (only pathname is used for caching). + * - If a list of parameter names, only those parameters are included in the cache key. + */ + allowQuery?: string[]; } diff --git a/test/fixture/api/cached-allow-query.ts b/test/fixture/api/cached-allow-query.ts new file mode 100644 index 0000000000..d0b397f649 --- /dev/null +++ b/test/fixture/api/cached-allow-query.ts @@ -0,0 +1,8 @@ +export default defineCachedEventHandler( + (event) => { + return { + timestamp: Date.now(), + }; + }, + { swr: true, maxAge: 60, allowQuery: ["q"] } +); diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index 8e7b56d1fa..1dea29e754 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -94,6 +94,9 @@ export default defineNitroConfig({ "/rules/redirect": { redirect: "/base" }, "/rules/isr/**": { isr: { allowQuery: ["q"] } }, "/rules/isr-ttl/**": { isr: 60 }, + "/rules/allow-query/**": { + cache: { swr: true, maxAge: 60, allowQuery: ["q"] }, + }, "/rules/swr/**": { swr: true }, "/rules/swr-ttl/**": { swr: 60 }, "/rules/redirect/obj": { diff --git a/test/tests.ts b/test/tests.ts index 6518d22aa3..8092515b8b 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -755,6 +755,27 @@ export function testNitro( } } ); + + it.skipIf(ctx.isIsolated || (isWindows && ctx.preset === "nitro-dev"))( + "allowQuery should ignore unlisted query params in cache key", + async () => { + const { data: first } = await callHandler({ + url: "/api/cached-allow-query?q=search&utm_source=email", + }); + + // Same q param, different unlisted param should hit cache + const { data: second } = await callHandler({ + url: "/api/cached-allow-query?q=search&utm_source=twitter", + }); + expect(second.timestamp).toBe(first.timestamp); + + // Different q param should get a different cache entry + const { data: third } = await callHandler({ + url: "/api/cached-allow-query?q=other&utm_source=email", + }); + expect(third.timestamp).not.toBe(first.timestamp); + } + ); }); describe("scanned files", () => { From 6d84ed411d49047e6de06f4417b30c97f9d7680e Mon Sep 17 00:00:00 2001 From: Florian Streise Date: Fri, 6 Mar 2026 19:49:16 +0100 Subject: [PATCH 2/3] fix: accept `readonly string[]` for `allowQuery` type Route rules config passes `allowQuery` as `readonly string[]`, so the type needs to accept both mutable and readonly arrays (matching `varies`). Co-Authored-By: Claude Opus 4.6 --- src/types/runtime/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/runtime/cache.ts b/src/types/runtime/cache.ts index 239f0e22c2..888ab099c6 100644 --- a/src/types/runtime/cache.ts +++ b/src/types/runtime/cache.ts @@ -43,5 +43,5 @@ export interface CachedEventHandlerOptions extends Omit< * - If an empty array `[]`, all query parameters are ignored (only pathname is used for caching). * - If a list of parameter names, only those parameters are included in the cache key. */ - allowQuery?: string[]; + allowQuery?: string[] | readonly string[]; } From fcd39ec5c5abf195deaf16a469c44fa768cfb1dc Mon Sep 17 00:00:00 2001 From: Florian Streise Date: Fri, 6 Mar 2026 19:55:00 +0100 Subject: [PATCH 3/3] test: update Vercel and Netlify legacy preset snapshots for allowQuery Co-Authored-By: Claude Opus 4.6 --- test/presets/netlify-legacy.test.ts | 35 +++++++++++++++-------------- test/presets/vercel.test.ts | 11 +++++++++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/test/presets/netlify-legacy.test.ts b/test/presets/netlify-legacy.test.ts index 2b3733599d..431778bd56 100644 --- a/test/presets/netlify-legacy.test.ts +++ b/test/presets/netlify-legacy.test.ts @@ -57,23 +57,24 @@ describe("nitro:preset:netlify-legacy", async () => { ); expect(redirects).toMatchInlineSnapshot(` - "/rules/nested/override /other 302 - /rules/redirect/wildcard/* https://nitro.build/:splat 302 - /rules/redirect/obj https://nitro.build/ 301 - /rules/nested/* /base 302 - /rules/redirect /base 302 - /rules/_/cached/noncached /.netlify/functions/server 200 - /rules/_/noncached/cached /.netlify/builders/server 200 - /rules/_/cached/* /.netlify/builders/server 200 - /rules/_/noncached/* /.netlify/functions/server 200 - /rules/swr-ttl/* /.netlify/builders/server 200 - /rules/swr/* /.netlify/builders/server 200 - /rules/isr-ttl/* /.netlify/builders/server 200 - /rules/isr/* /.netlify/builders/server 200 - /rules/dynamic /.netlify/functions/server 200 - /build/* /build/:splat 200 - /* /.netlify/functions/server 200" - `); + "/rules/nested/override /other 302 + /rules/redirect/wildcard/* https://nitro.build/:splat 302 + /rules/redirect/obj https://nitro.build/ 301 + /rules/nested/* /base 302 + /rules/redirect /base 302 + /rules/_/cached/noncached /.netlify/functions/server 200 + /rules/_/noncached/cached /.netlify/builders/server 200 + /rules/_/cached/* /.netlify/builders/server 200 + /rules/_/noncached/* /.netlify/functions/server 200 + /rules/swr-ttl/* /.netlify/builders/server 200 + /rules/swr/* /.netlify/builders/server 200 + /rules/allow-query/* /.netlify/builders/server 200 + /rules/isr-ttl/* /.netlify/builders/server 200 + /rules/isr/* /.netlify/builders/server 200 + /rules/dynamic /.netlify/functions/server 200 + /build/* /build/:splat 200 + /* /.netlify/functions/server 200" + `); }); it("should add route rules - headers", async () => { const headers = await fsp.readFile( diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index 4d3a8c5416..e1d1e596cf 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -141,6 +141,10 @@ describe("nitro:preset:vercel", async () => { "dest": "/rules/isr-ttl/[...]-isr?__isr_route=$__isr_route", "src": "(?<__isr_route>/rules/isr-ttl/(?:.*))", }, + { + "dest": "/rules/allow-query/[...]-isr?__isr_route=$__isr_route", + "src": "(?<__isr_route>/rules/allow-query/(?:.*))", + }, { "dest": "/rules/swr/[...]-isr?__isr_route=$__isr_route", "src": "(?<__isr_route>/rules/swr/(?:.*))", @@ -337,6 +341,10 @@ describe("nitro:preset:vercel", async () => { "dest": "/api/db", "src": "/api/db", }, + { + "dest": "/api/cached-allow-query", + "src": "/api/cached-allow-query", + }, { "dest": "/api/cached", "src": "/api/cached", @@ -469,6 +477,7 @@ describe("nitro:preset:vercel", async () => { "functions/__fallback.func/node_modules", "functions/__fallback.func/package.json", "functions/__fallback.func/timing.js", + "functions/api/cached-allow-query.func (symlink)", "functions/api/cached.func (symlink)", "functions/api/db.func (symlink)", "functions/api/echo.func (symlink)", @@ -531,6 +540,8 @@ describe("nitro:preset:vercel", async () => { "functions/rules/_/cached/[...]-isr.prerender-config.json", "functions/rules/_/noncached/cached-isr.func (symlink)", "functions/rules/_/noncached/cached-isr.prerender-config.json", + "functions/rules/allow-query/[...]-isr.func (symlink)", + "functions/rules/allow-query/[...]-isr.prerender-config.json", "functions/rules/isr-ttl/[...]-isr.func (symlink)", "functions/rules/isr-ttl/[...]-isr.prerender-config.json", "functions/rules/isr/[...]-isr.func (symlink)",