Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support serializable options to configure caching route rules #1048

Open
pi0 opened this issue Mar 13, 2023 · 21 comments
Open

Support serializable options to configure caching route rules #1048

pi0 opened this issue Mar 13, 2023 · 21 comments
Assignees
Labels

Comments

@pi0
Copy link
Member

pi0 commented Mar 13, 2023

Related: #977

Since route rules only accept serializable options, and for best DX, we can introduce a configuration API that defines caching route rules as an alternative to non-serializable options (shouldInvalidateCache ~> invalidateHeaders, getKey ~> keyTemplate)

@ah-dc
Copy link

ah-dc commented May 24, 2023

I am concerned that keyTemplate is not flexible enough to replace getKey.
How about defining custom cache rulesets and handlers in code and just specify the ruleset id in configuration?

@gazreyn
Copy link

gazreyn commented May 31, 2023

I believe this is something that I need but I may be misunderstanding so sorry if I am.

My use case is that I'm using ISR to keep my site cached as I'm using a headless CMS to serve content. I'm using ISR as a way to serve the content ASAP. However, I'd also like it if a user publishes new content/changes in Storyblok (CMS), i could selectively/programmatically invalidate the cache so that the published changes are reflected in the live site.

Not sure if the above use case is related to this but it was something I've been searching for and this seemed the closest.

@lxccc812

This comment was marked as off-topic.

@jony1993
Copy link

We are exploring solutions to bypass caching for logged-in users within our Nuxt 3 application. It would be ideal to have a feature that allows specifying conditions under which route caching should be bypassed, such as the presence of an authentication cookie.

@pi0 Is there currently any method or workaround available to selectively disable caching under certain conditions? For instance, could we leverage Nitro server middleware to achieve this?

@pi0
Copy link
Member Author

pi0 commented Mar 29, 2024

@jony1993 Not with route rules yet but you can simply replace route rule with defineCachedEventHandler with shouldInvalidateCache.

@MiniDigger
Copy link
Contributor

MiniDigger commented Mar 29, 2024

@jony1993 I just wrote a nitro plugin that imports the nuxt handler and manually wraps it with a cachedEventHandler call and pass the options that way

@jony1993
Copy link

jony1993 commented Mar 29, 2024

@pi0 Thanks for the quick response.

You mean something like this? (for example in server/middleware/cache.ts)

export default defineCachedEventHandler(() => {}, {
  swr: true,
  maxAge: 60 * 60,
  shouldBypassCache: (event) => {
    // Check for an authentication cookie to determine whether to bypass the cache
    const auth = getCookie(event, 'auth')
    try {
      // Attempt to parse the cookie string into an object
      const authCookieObject = JSON.parse(auth)

      // Check the isAuthenticated property
      return authCookieObject.isAuthenticated
    }
    catch (error) {
      // In case of error (e.g., parsing error), assume the user is not authenticated
      // Bypass the cache if we can't parse the cookie
      return false
    }
  },
})

@swissmexxa
Copy link

swissmexxa commented May 8, 2024

@pi0 so how would this look like? Version of @jony1993 is not working. I'm using Nitro with Nuxt3

@swissmexxa
Copy link

@MiniDigger Could you share the code of your plugin you mentioned above?

@MiniDigger
Copy link
Contributor

MiniDigger commented May 10, 2024

@swissmexxa its, uhhm, not pretty but it works, lol
been running this in prod for like a year now, it kinda goes like this

export default defineNitroPlugin((nitroApp) => {
  // find our handler
  const h = (eval("handlers") as HandlerDefinition[]).find((ha) => ha.route === "/**");
  if (!h) {
    nitroLog("Not enabling ISG cause we didn't find a matching handler");
    return;
  }

  const handler = cachedEventHandler(lazyEventHandler(h.handler), {
      group: "pages",
      swr: false,
      maxAge: 30 * 60,
      name: "pagecache",
      getKey,
      shouldInvalidateCache,
      shouldBypassCache,
    } as CacheOptions);

  nitroLog("installing handler", h.route);
  nitroApp.router.use(h.route, handler, h.method);

and then I just have methods like this:

function shouldInvalidateCache(e: H3Event)
function shouldBypassCache(e: H3Event)
function getKey(e: H3Event)

bascially, I rely on nitro packaging the handles and my plugin into the same file and that nitro defines a handlers variable in that file, in an accessible scope. then I just need to find my handler (which is the one with the route /**) and can reregister it into the nitro router with my desired settings.
need to use eval so that vite doesn't rename the handlers variable for conflict.
hope this helps.

@lxccc812
Copy link

lxccc812 commented May 14, 2024

@swissmexxa its, uhhm, not pretty but it works, lol been running this in prod for like a year now, it kinda goes like this

export default defineNitroPlugin((nitroApp) => {
  // find our handler
  const h = (eval("handlers") as HandlerDefinition[]).find((ha) => ha.route === "/**");
  if (!h) {
    nitroLog("Not enabling ISG cause we didn't find a matching handler");
    return;
  }

  const handler = cachedEventHandler(lazyEventHandler(h.handler), {
      group: "pages",
      swr: false,
      maxAge: 30 * 60,
      name: "pagecache",
      getKey,
      shouldInvalidateCache,
      shouldBypassCache,
    } as CacheOptions);

  nitroLog("installing handler", h.route);
  nitroApp.router.use(h.route, handler, h.method);

and then I just have methods like this:

function shouldInvalidateCache(e: H3Event)
function shouldBypassCache(e: H3Event)
function getKey(e: H3Event)

bascially, I rely on nitro packaging the handles and my plugin into the same file and that nitro defines a handlers variable in that file, in an accessible scope. then I just need to find my handler (which is the one with the route /**) and can reregister it into the nitro router with my desired settings. need to use eval so that vite doesn't rename the handlers variable for conflict. hope this helps.

Please forgive me for not understanding the example you wrote. In which folder is this example written? What if I want to customize the cache key based on the full URL of the current page, or non-parameters in the URL?

I tried printing the path information, but unfortunately, it didn't output anything.
image

@swissmexxa
Copy link

@MiniDigger Thank you very much for your example

@lxccc812 This file would be placed inside the server/plugins folder. The defineCachedEventHandler function has an option object as second parameter where you can set a getKey function. This function will receive the event object as parameter where you can get the request URL and Query parameter as follow:

function getKey(event: Event): string {
  const currentUrl = getRequestURL(event);
  const queries = getQuery(event);
  ... your own logic
  return theKey;
}

Then you can create your own logic for creating a string key and return it inside the getKey function.

I have found a quite simple solution to just disable the generation of the Nuxt cache under certain conditions in Nuxt 3 with routeRules if this already helps you:

server/plugins/nuxt-cache-invalidator.ts

export default defineNitroPlugin(nitroApp => {
  nitroApp.hooks.hook('render:response', (response, { event }) => {
    const queries = getQuery(event);
    const shouldNotCreateCache = <HERE YOUR OWN LOGIC>

    if (shouldNotCreateCache)
      response['headers'] = {
        ...(response['headers'] ?? {}),
        'cache-control': 'no-store',
        'last-modified': 'undefined', // required so Nuxt cache is not valid and does not create a cache version
      };
    else
      response['headers'] = { ...(response['headers'] ?? {}), 'cache-control': 'max-age=300, stale-while-revalidate' };
  });
});

nuxt.config.ts

routeRules: {
    '/**': { cache: {} }, // do not set any props or values from plugin will be overwritten
  },

@lxccc812
Copy link

I used define Cache EventHandler to specify the value of key, but it doesn’t seem to work.
image

@swissmexxa
Copy link

swissmexxa commented May 15, 2024

@lxccc812 Define your routeRules directly in the root of your nuxt.config object and not under nitro.

I made a stackblitz example for you: https://stackblitz.com/edit/github-v24i7v-dipodp

You can call any sub route (ex. /i-am-a-cached-page) and it will create a cached page file under .nuxt/cache/nitro/routes/_. But if you add the query parameter ?dont-cache (ex. /do-not-cache-me?dont-cache it won't generate a cache file under .nuxt/cache/nitro/routes/_ for the page.

My solution is only to prevent the generation of a cache entry by Nuxt under certain conditions (for example when a user is logged in or you are in preview mode of a CMS with query params). If you want to also change the structure of the generated cache key you will have to have a look at the solution of MiniDigger.

Hope it helps

@lxccc812
Copy link

@lxccc812 Define your routeRules directly in the root of your nuxt.config object and not under nitro.

I made a stackblitz example for you: https://stackblitz.com/edit/github-v24i7v-dipodp

You can call any sub route (ex. /i-am-a-cached-page) and it will create a cached page file under .nuxt/cache/nitro/routes/_. But if you add the query parameter ?dont-cache (ex. /do-not-cache-me?dont-cache it won't generate a cache file under .nuxt/cache/nitro/routes/_ for the page.

My solution is only to prevent the generation of a cache entry by Nuxt under certain conditions (for example when a user is logged in or you are in preview mode of a CMS with query params). If you want to also change the structure of the generated cache key you will have to have a look at the solution of MiniDigger.

Hope it helps

Thank you for your patient answer, but for me, what I need more is how to customize the cache key name.

/a/b/[slug].vue
/a/b/c?id=1
/a/b/d?id=2
I usually need to get variables and params to customize the cache key name, I will look at the example written by MiniDigger again for research.

Thanks again, I also learned a lot from your answers.

@swissmexxa
Copy link

@lxccc812 I made you another stackblitz including an example of MiniDigger: https://stackblitz.com/edit/github-v24i7v-kkln93

You have the same behavior from the stackblitz of the previous comment but now there is additional code in server/plugins/cache-keys.ts. If you visit a subpage under /en/... it will use a custom cache key (using pathname and query params) and create a cache file under ./nuxt/cache/pages/en

@lxccc812
Copy link

@swissmexxa Thank you for your example. When I tested the example you gave me, I found that if I have multiple routes that need to be cached, how should I accurately obtain the handler relative to the page route?

I printed the handlerList, which includes all routes with cache declared in routeRules.

I modified your example a little bit, the address is here Example

Now I can only customize the cache key name of route a

@swissmexxa
Copy link

@lxccc812 with this

const enHandler = handlerList.find(
    (r) => r.route === '/a' || r.route === '/b'
  );

you search an element in a list where route is either /a or /b. So it will stop at /a because it comes first in the list and return it. Therefore it will always return the handler for /a and not /b.

You will have to define code for every route you define in routeRules.

rough example:

 const aHandler = handlerList.find((r) => r.route === '/a');
 const bHandler = handlerList.find((r) => r.route === '/b');

 if (aHandler) {
   customHandler = cachedEventHandler(...
   nitroApp.router.use(aHandler.route, customHandler, aHandler.method);
 if(bHandler) {
   customHandler = cachedEventHandler(...
   nitroApp.router.use(bHandler.route, customHandler, bHandler.method);
 }

For the example of an earlier comment above with these pages:

/a/b/[slug].vue
/a/b/c?id=1
/a/b/d?id=2

The routeRules would be

'/**': { cache: {} }, // matching all routes except more specific below
'/a/**': { cache: {} }, // matching all subroutes of a/ except more specific below
'/a/b/**': { cache: {} }, // matching all subroutes of a/b/ except more specific below
'/a/b/c': { cache: {} }, // matching exact subroute a/b/c

and would get handlers as follow:

 const starHandler = handlerList.find((r) => r.route === '/**');
 const aStarHandler = handlerList.find((r) => r.route === '/a/**');
 const abStarHandler = handlerList.find((r) => r.route === '/a/b/**');
 const abcHandler = handlerList.find((r) => r.route === '/a/b/c');

or you could just add one routeRule /** with one handler and decide in the getKey method of the one handler how the key should be created based on the path which you can get with the H3Event parameter of the getKey function.

@lxccc812
Copy link

@swissmexxa Thank you again for your patient answer.

The sign to generate cache is to enter the getKey function, so what is the sign to use cache?In this example, when I click on any link, it goes into the function getKey and returns the custom string. Does this mean that I create the cache on each click instead of using the cache generated on the first click on the second click.

console.log
image

Does this mean that the cache has been used?
image

If I have two or more users accessing page/a on different devices, then when the different users access the page again (a second request to the server), will they retrieve it from cache? Return a response to reduce server pressure.

@lxccc812
Copy link

I found a new problem. If I access ?t=2 and successfully generate cache with t variable, then when I access ?t=2&x=1 again, it will automatically jump to ?t=2, which is not what I want. What I need is access to ?t=2&x=1 or other similar links with multiple indeterminate parameters. It will return the cache of ?t=2 and keep the original link without redirecting or jumping to the ?t=2 link.

image

You can see a related demo here

I also wrote a workaround, but I'm not sure if it will bypass the cache and access the server directly.
My understanding of this sentence from the documentation: "A function that returns a boolean value to bypass the current cache without invalidating existing entries". That is, bypassing the step of generating the cache and continuing to use the current cache.If I understand correctly, my method is valid.

shouldBypassCache: (event: H3Event) => {

  const queries = getQuery(event);

  const cacheKeys = ['t'];

  const queryKeys = Object.keys(queries);

  const hasOtherQuery = queryKeys.some(
    (queryKey) => !cacheKeys.includes(queryKey)
  );

  return hasOtherQuery;
},

@johnjenkins
Copy link

johnjenkins commented Sep 27, 2024

in lieu of an official solution - I thought I'd add what I've done (building on other contributors' insights here):

// nuxt.config.ts
...
routeRules: {
  '/your-path/**': {
    cache: {} // just a placeholder that we can augment in our server-plugin
  }
}
// server/plugins/your-handler.ts 

import { parseURL } from 'ufo'
import { hash } from 'ohash'
// @ts-expect-error virtual file
import { handlers } from '#internal/nitro/virtual/server-handlers' // eslint-disable-line import/no-unresolved
import type { HandlerDefinition } from 'nitropack/runtime/virtual/server-handlers'

function escapeKey(key: string | string[]) {
  return String(key).replace(/\W/g, '')
}

export default defineNitroPlugin(nitroApp => {
  const foundHandler = (handlers as HandlerDefinition[]).find(
    ha => ha.route === '/your-path/**' // < Whatever pattern you're looking to cache
  )
  if (!foundHandler) return

  const handler = cachedEventHandler(lazyEventHandler(foundHandler.handler), {
    swr: true,
    maxAge: 3600,

    getKey: async event => {
      // This is mainly yoinked from the default nitro `getKey`
      // (https://github.com/unjs/nitro/blob/v2/src/runtime/internal/cache.ts - pathname + hashed props)
      
      const path =
        event.node.req.originalUrl || event.node.req.url || event.path
      const decodedPath = decodeURI(parseURL(path).pathname)

      const pathname =
        escapeKey(decodeURI(parseURL(path).pathname)).slice(0, 16) || 'index'

      const hashedPath = `${pathname}.${hash(path)}.${ANYTHING_ELSE_YOU_WANT_HERE_USING_HEADERS_OR_WHATEVER}`
      return hashedPath
    },
  })

  nitroApp.router.use(foundHandler.route, handler, foundHandler.method)
  console.info('installed routeRules cache handler', foundHandler.route)
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants