nuxt-multi-cache icon indicating copy to clipboard operation
nuxt-multi-cache copied to clipboard

Route caching with jwt token cookie

Open milelazar opened this issue 10 months ago • 4 comments

Hi,

I have a problem with cookies when using route caching and not sure how to solve it. On first load, if cookie does not already exist, jwt token is fetched server side from external API and stored to cookie.

When route cache is enabled, cookie is added to headers so when another user opens the same page, his jwt token cookie is overriden with the cookie from cached page header. I have tried to remove cookie from headers using alterCachedHeaders

alterCachedHeaders(headers) {
	headers['set-cookie'] = undefined;
	return headers;
},

It kinda works although not sure if this is the correct way. But sometimes when cached page is loaded for new session, server cookie is created and not passed to client so i end up in infinite loop of trying to generate new token since client side fetch requests are not authorized. This is not an issue when caching is disabled.

Am I missing something? Is there some "official" way of handling this? Each user should keep his unique jwt cookie token.

milelazar avatar Feb 13 '25 23:02 milelazar

This is a tricky topic: It depends on what you want to achieve. If during SSR you render anything user related (such as their user name), then you must vary the cache for each user or else user B would receive a cached route rendered for user A. However, if you start to vary the cache for every user, the benefits of SSR + caching are basically gone: If you have 100 pages and 100 users, you could end up with a max of 10,000 cached routes for every combination.

If you don't render anything user related during SSR, but instead wrap everything user related in <ClientOnly>, then you can technically cache without varying by user. But then you might as well keep anything that relates to authentication strictly to the client (e.g. fetch the JWT only client side). This would keep your server side stuff completely clean: The only state you'd have to care about on the server is for a state that does not take any authentication/token into account.

Personally I always disable any form of caching for "logged in users" to avoid any problems. My reasoning is that the benefit of SSR is mostly for SEO and faster page loads for anonymous users. Once a user is authenticated, what's the benefit of serving them a cached route? However if all your users always are authenticated (or have a token), then I can see why you'd still want SSR.

Anyway, in your specific case, using alterCachedHeaders is technically the correct way to remove headers from what is stored in the cache. This is executed after the response has been sent to the client. But, you say:

But sometimes when cached page is loaded for new session, server cookie is created and not passed to client

What I can imagine is happening is the following scenario:

  • App is started with an empty cache
  • User A visits /page
  • During SSR you set a set-cookie header
  • Response is sent to the client, including set-cookie
  • The module calls alterCachedHeaders, you remove set-cookie, the route is stored in cache
  • User B visits /page
  • Since there is a cache entry for /page, the module returns the cached route as the response. No further code in your app is executed (e.g. no set-cookie header is set)

My question would be: Are you sure that in this case you are actually creating a token and setting the set-cookie header?

dulnan avatar Feb 15 '25 06:02 dulnan

Thank you for the extensive answer. I have changed approach and did some trade offs so i can generate the token client side and avoid problems. Sorry for the long post but to give you a bigger picture, let me share more details.

This is a large ecoomerce site and i need to have cached routes even for logged in users (there are bunch of both logged in and anonymous users browsing) to reduce server load. User specific content is wrapped with Nuxt client only tag (like cart, wishlist, etc)

  1. On first load nuxt plugin is used to fetch user token (if token cookie does not already exists) and additional initial data server side from external API
  2. Token is saved to cookie and every further API fetch request is authenticated with that token
  3. Product page is generated server side for SEO purposes and cached. Nuxt middleware is used to figure out what page/template should be presented to the user (catch all page with dynamic components)

So now we get to the tricky part in my use case

  1. Products and blog posts have "status" property and might be hidden for everyone except for logged in user with admin role. Everyone else gets a 404 page
  2. Since product page is rendered server side, I also need to check if user is logged in server side to be able to redirect or show the content. Otherwise in best case you get 404 flash and hydration warnings when client side kicks in and user role can be checked

I figured out that when route is cached and you get cached page, plugins and middlewares are no longer triggered server side. Which makes sense. But since token generating logic is not triggered, new user ends up without a token entering 401 loop. I have also tried to use Nitro plugin. The same thing. Tried Nitro middleware. Event 2 can be executed before token is generated and passed in Event 1 so you end up with multiple new tokens.

Not sure if this is even possible, but it would be great to have some sort of hook before cached response is returned, where "something" can be executed on server without affecting cached results.

When using default Nuxt routeRules, user cookie token gets cached with route so when you open a cached page, your cookie token gets replaced with someone else stored to cached response. There are "varies" options where you can vary cache based on cookie. But it makes no sense to cache data just for yourself.

Workaround solution Now I have a bit hacky solution which is not ideal but it works. I have two tokens. First one is only used on server to get initial data that is the same for all users and does not affect route caching. Another is generated client side for every user. process.server/client is used in middleware to handle status problems

milelazar avatar Feb 15 '25 11:02 milelazar

The module adds its own event handler that serves a route from cache: https://github.com/dulnan/nuxt-multi-cache/blob/main/src/runtime/server/handler/serveCachedRoute.ts#L41

The handler is added via a nitro plugin here: https://github.com/dulnan/nuxt-multi-cache/blob/89036d8a330a025caa93e58f2df87fdcbd6dbc62/src/runtime/server/plugins/multiCache.ts#L56

The reason it's done like this is to make sure that the handler runs as early as possible, before any other custom event handlers.

Not sure if this is even possible, but it would be great to have some sort of hook before cached response is returned, where "something" can be executed on server without affecting cached results.

This is in fact possible! You can add a Nitro plugin and use the beforeResponse hook:

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('beforeResponse', (event) => {
    setResponseHeader(event, 'set-cookie', 'your-cookie-value')
  })
})

This will be executed before any response, both cached and uncached.

dulnan avatar Feb 15 '25 12:02 dulnan

Tried this already. beforeResponse hook acts the same as middleware. It is executed on every request. So I end up with multiple new tokens like described before. There is no way to "await" before proceeding to another event so the same token can be shared.

Will try few more ideas and get back if something positive comes out of it

milelazar avatar Feb 15 '25 14:02 milelazar

Closing this, please let me know if (and how) you managed to resolve your issue, would be interested to know. Also I'm happy to assist further if needed if it's not yet working as expected.

dulnan avatar Jul 13 '25 15:07 dulnan