Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/1.guide/6.cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion src/runtime/internal/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
7 changes: 7 additions & 0 deletions src/types/runtime/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,11 @@ export interface CachedEventHandlerOptions<T = any> 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[] | readonly string[];
}
8 changes: 8 additions & 0 deletions test/fixture/api/cached-allow-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default defineCachedEventHandler(
(event) => {
return {
timestamp: Date.now(),
};
},
{ swr: true, maxAge: 60, allowQuery: ["q"] }
);
3 changes: 3 additions & 0 deletions test/fixture/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
35 changes: 18 additions & 17 deletions test/presets/netlify-legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions test/presets/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/(?:.*))",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)",
Expand Down Expand Up @@ -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)",
Expand Down
21 changes: 21 additions & 0 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading