From 61e4c3ff990d93c8b8531a89ce2368d83397f875 Mon Sep 17 00:00:00 2001 From: Florian Streise Date: Fri, 6 Mar 2026 19:45:00 +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 `defineCachedHandler` 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.docs/7.cache.md | 4 ++++ src/runtime/internal/cache.ts | 15 ++++++++++++- src/types/runtime/cache.ts | 7 +++++++ test/fixture/nitro.config.ts | 1 + .../server/routes/api/cached-allow-query.ts | 10 +++++++++ test/tests.ts | 21 +++++++++++++++++++ 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/fixture/server/routes/api/cached-allow-query.ts diff --git a/docs/1.docs/7.cache.md b/docs/1.docs/7.cache.md index b1caea9515..c0bfd8cf92 100644 --- a/docs/1.docs/7.cache.md +++ b/docs/1.docs/7.cache.md @@ -223,6 +223,10 @@ The `defineCachedHandler` and `defineCachedFunction` functions accept the follow ::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 052459902d..51a4f8f5cd 100644 --- a/src/runtime/internal/cache.ts +++ b/src/runtime/internal/cache.ts @@ -201,7 +201,20 @@ export function defineCachedHandler( return escapeKey(customKey); } // Auto-generated key - const _path = event.url.pathname + event.url.search; + let _path: string; + if (opts.allowQuery) { + const params = new URLSearchParams(); + for (const key of opts.allowQuery) { + const value = event.url.searchParams.get(key); + if (value !== null) { + params.set(key, value); + } + } + const search = params.size > 0 ? `?${params.toString()}` : ""; + _path = event.url.pathname + search; + } else { + _path = event.url.pathname + event.url.search; + } let _pathname: string; try { _pathname = escapeKey(decodeURI(parseURL(_path).pathname)).slice(0, 16) || "index"; diff --git a/src/types/runtime/cache.ts b/src/types/runtime/cache.ts index 88fd991f7f..bcb29eff9c 100644 --- a/src/types/runtime/cache.ts +++ b/src/types/runtime/cache.ts @@ -38,4 +38,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/nitro.config.ts b/test/fixture/nitro.config.ts index ba9b84b47c..b0c090752e 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -90,6 +90,7 @@ export default defineConfig({ }, "/rules/dynamic": { cache: false, isr: false }, "/rules/redirect": { redirect: "/base" }, + "/rules/allow-query/**": { cache: { swr: true, maxAge: 60, allowQuery: ["q"] } }, "/rules/isr/**": { isr: { allowQuery: ["q"] } }, "/rules/isr-ttl/**": { isr: 60 }, "/rules/swr/**": { swr: true }, diff --git a/test/fixture/server/routes/api/cached-allow-query.ts b/test/fixture/server/routes/api/cached-allow-query.ts new file mode 100644 index 0000000000..1160961d1c --- /dev/null +++ b/test/fixture/server/routes/api/cached-allow-query.ts @@ -0,0 +1,10 @@ +import { defineCachedHandler } from "nitro/cache"; + +export default defineCachedHandler( + (event) => { + return { + timestamp: Date.now(), + }; + }, + { swr: true, maxAge: 60, allowQuery: ["q"] } +); diff --git a/test/tests.ts b/test/tests.ts index 684406edc3..8f4e224384 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -692,6 +692,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 c0fd5d534ccf130b45e067dba90c308f23cece8a Mon Sep 17 00:00:00 2001 From: Florian Streise Date: Fri, 6 Mar 2026 19:49:09 +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 bcb29eff9c..963a19bed3 100644 --- a/src/types/runtime/cache.ts +++ b/src/types/runtime/cache.ts @@ -44,5 +44,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 3e54b26676e1ac0cef97b6a350fc45a66ce17ad1 Mon Sep 17 00:00:00 2001 From: Florian Streise Date: Fri, 6 Mar 2026 19:53:40 +0100 Subject: [PATCH 3/3] test: update Vercel preset snapshots for allowQuery Co-Authored-By: Claude Opus 4.6 --- test/presets/vercel.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index 732d2ebd33..3ce8ae1ee2 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -151,6 +151,10 @@ describe("nitro:preset:vercel:web", async () => { "dest": "/__server", "src": "(?<__isr_route>/rules/dynamic)", }, + { + "dest": "/rules/allow-query/[...]-isr?__isr_route=$__isr_route", + "src": "(?<__isr_route>/rules/allow-query/(?:.*))", + }, { "dest": "/rules/isr/[...]-isr?__isr_route=$__isr_route", "src": "(?<__isr_route>/rules/isr/(?:.*))", @@ -319,6 +323,10 @@ describe("nitro:preset:vercel:web", async () => { "dest": "/api/db", "src": "/api/db", }, + { + "dest": "/api/cached-allow-query", + "src": "/api/cached-allow-query", + }, { "dest": "/api/cached", "src": "/api/cached", @@ -418,6 +426,7 @@ describe("nitro:preset:vercel:web", async () => { "functions/_scalar.func (symlink)", "functions/_swagger.func (symlink)", "functions/_vercel", + "functions/api/cached-allow-query.func (symlink)", "functions/api/cached.func (symlink)", "functions/api/db.func (symlink)", "functions/api/echo.func (symlink)", @@ -460,6 +469,8 @@ describe("nitro:preset:vercel:web", 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)",