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

Incomplete/incorrectly cached pages? #18

Open
iparker opened this issue Jun 12, 2023 · 12 comments
Open

Incomplete/incorrectly cached pages? #18

iparker opened this issue Jun 12, 2023 · 12 comments

Comments

@iparker
Copy link

iparker commented Jun 12, 2023

Hello,

I'm currently testing the plugin in the V1 version, there will still be a Nuxt2 application.

I often have the problem that individual pages are not cached correctly. It appears that certain elements are simply missing, e.g. because a background image or buttons are not displayed correctly. Also the dropdown of the main navigation does not work then.

In Sentry I see the following error:

HierarchyRequestError
Object.appendChild(97f3cfd.modern)
Failed to execute 'appendChild' on 'Node': This node type does not support this method.

We have 4 node instances on our server in parallel operation. Here I can see that the page is displayed correctly in some node instances and the error occurs in other instances. I can see this because as a test I appended a Math.random() to the title of the page.

Unfortunately I can't get any further and I don't know how to solve the problem.

I would be grateful for any tips and help!

Best regards

Timo

@dulnan
Copy link
Owner

dulnan commented Jun 12, 2023

Which caches are you using? For the page cache it's very important that a page renders exactly the same for a given path. I've seen it happen where state in the Nuxt app is set based on request headers or other factors, for example to determine the language. This won't be part of the cache key and will lead to serving a page rendered in language A, but client side code decides language B should be used, leading to hydration mismatch.

Also keep in mind that V1 does not properly support running on multiple instances. The cache is done in-memory, so each instance has its own cache. When you invalidate the cache using the API it will only invalidate it in the instance that responds to the API request. You would have to somehow implement a way to execute the API request against each of the 4 node instances.

In addition, when using component cache, it's also important that a cached component exclusively gets its data via props (e.g. no Vuex, this.$route, etc.), so that the cache key can be determined given the props. Using external state can lead to such bugs as well.

@iparker
Copy link
Author

iparker commented Jun 12, 2023

Many thanks for the quick response!

I currently have the pageCache, the componentCache and the dataCache enabled, but I'm only testing the caching of pages and now also the first components.

On the Page I set the caching behavior in the asyncData:

app.$multicache.route.setCacheable();
app.$multicache.route.addTags(['eventjobs_index']);

[Note: I have injected $cache to $multicache because another plugin has used $cache]

When I query the pages in the cache via the API I get:

{
    "rows": [
        {
            "tags": [
                "eventjobs_index"
            ],
            "time stamp": 1686566506214,
            "key": "/event jobs"
        }
    ],
    "total": 1
}

Also keep in mind that V1 does not properly support running on multiple instances. The cache is done in memory, so each instance has its own cache. When you invalidate the cache using the API it will only invalidate it in the instance that responds to the API request. You would have to somehow implement a way to execute the API request against each of the 4 node instances.

Yes, I had this thought earlier as well. I need to talk to a colleague again about this.

I've just started with the component cache, and unfortunately I also have the problem here that the contents of the cache are not output correctly. A component is probably not suitable for caching if it contains something like:

<client only>
  <LoggedUserMobileMenuDialog v-if="$device.isMobile && $auth.loggedIn" />
</client only>

I don't understand the error in the output at all. It seems that it still works in the first SSR requests and after a few refeshs no more navigation points are output in the footer. Unfortunately, I don't understand why this behavior is like this. It looks as if the whole component no longer works, also in CSR.

@dulnan
Copy link
Owner

dulnan commented Jun 12, 2023

One thing you could try is to disable JavaScript temporarily in your browser, refresh several times and check if the navigation points are actually contained in the SSR markup and thus disappear due to client side JS.

v-if="$device.isMobile && $auth.loggedIn"

This would definitely fall in the category of not allowed in a cached component 😄 I think this might also be what causes the issues with the page cache: A first request to /page-1 might be from a mobile device with loggedIn being true. The page is then rendered accordingly, with certain components being visible. The rendered markup is put into the cache. The second request to /page-1 from another user, using a desktop device and loggedIn being false will now be served the cached page not suitable for them. This will almost certainly lead to hydration issues.

Now, this is not an unsolvable problem: The module provides a way for you to manually build the cache key for the page. By default, the cache key in our example would be /page-1, which does not take into account the other factors. So instead the first request should have a cache key of /page-1--mobile--loggedIn and the second request /page-1--desktop--anonymous.

You can do this by providing a custom getCacheKey method:

nuxt.config.ts

function getCacheKey(route: string, context: any) {
  const req = context.req as Request
  const path = req.originalUrl
  const isLoggedIn = true // Determine the auth status.
  const isMobileDevice = true // Determine the device type.
  return [
    path,
    isLoggedIn ? 'loggedIn' : 'anonymous',
    isMobileDevice ? 'mobile' : 'desktop'
  ].join('--')
}

export default {
  multiCache: {
    enabled: true,
    pageCache: {
      enabled: true,
      getCacheKey
    },
  }
}

Unfortunately it's not possible to access $device or $auth at this stage - the page cache hooks into a very early stage, where the Nuxt app has not yet been initialized, to return cached markup. You would have to reimplement parts of the logic that determines the device type and auth status here.

For the component cache it's pretty similar, but much easier, because you can pass isMobile or isLoggedIn as props to the cached component and use these directly as the cache key.

Now, there is also another way to solve the problem. This is also what I do in my projects. Logged in users don't ever get a cached page and also do not put anything into the cache. For that there is the enabledForRequest method which you can implement. If you implement this and return false here, the request will not be served from cache and also won't be put into the cache.

@iparker
Copy link
Author

iparker commented Jun 12, 2023

Thanks again for your quick reply!

This would definitely fall in the category of not allowed in a cached component smile I think this might also be what causes the issues with the page cache: A first request to /page-1 might be from a mobile device with loggedIn being true.

Do you mean that if a page has a component which is based on user data (like in this case) this leads in errors with the page?

Could a solution be to set these components to client-only?

For the component cache it's pretty similar, but much easier, because you can pass isMobile or isLoggedIn as props to the cached component and use these directly as the cache key.

That's a good idea, but the component caching was completely not working for me :-(.

Now, there is also another way to solve the problem. This is also what I do in my projects. Logged in users don't ever get a cached page and also do not put anything into the cache. For that there is the enabledForRequest method which you can implement. If you implement this and return false here, the request will not be served from cache and also won't be put into the cache.

Yes, I read about this in the new version docs at https://nuxt-multi-cache.dulnan.net/advanced/enable-cache-conditionally. I will give this a try, but also have the problem with passing $auth at this point, right?

@dulnan
Copy link
Owner

dulnan commented Jun 12, 2023

Do you mean that if a page has a component which is based on user data (like in this case) this leads in errors with the page?

Yes, I'm pretty confident that this is what's causing the issues. The reason is that during hydration Nuxt expects certain things to be there, based on the state that has been determined when the Nuxt app is initialized client side. This state is now different than the one being used to render the page SSR (likely from a different request). So there is a mismatch between what's in the DOM from SSR and what Nuxt expects.

Could a solution be to set these components to client-only?

Yes, that would be a possible solution as well!

That's a good idea, but the component caching was completely not working for me :-(.

I also think that this relates to the global state being used in the component. You could try it with a very simple component, that only renders text, to see if you get that working.

I will give this a try, but also have the problem with passing $auth at this point, right?

That's true - but in my case I was able to determine the logged in status based on the presence of a certain cookie. If you're using nuxt-auth, this should be pretty straight forward, by default the cookie is prefixed auth. I think. So if the cookie is present, you can bail out of cache for the request.

@iparker
Copy link
Author

iparker commented Jun 12, 2023

That's true - but in my case I was able to determine the logged in status based on the presence of a certain cookie. If you're using nuxt-auth, this should be pretty straight forward, by default the cookie is prefixed auth. I think. So if the cookie is present, you can bail out of cache for the request.

Sorry - I have to re-ask for this.

I don't see how to do a cookie check in nuxt.config (because the nuxt config is just used in build process i think).

But I have to define the enabledForRequest just inside the nuxt.config, right?

Sorry but I don't see/understand how to solve this. Maybe you have a litte code example for me?

@dulnan
Copy link
Owner

dulnan commented Jun 12, 2023

In Nuxt 2 the code from nuxt.config.js can be used during runtime of the application, so you can put the methods there. This has changed in Nuxt 3.

The docs for V1 are a bit weird, sorry 😄 Here's an example:

export default {
  multiCache: {
    enabled: true,
    enabledForRequest: function(req) {
      if (req.headers.cookie) {
        return req.headers.cookie.includes('auth.')
      }
      return true
    },
    pageCache: {
      enabled: true,
    },
  }
}

@iparker
Copy link
Author

iparker commented Jun 12, 2023

Sorry that I have to ask you something again - but I'm stuck here with problems that I don't understand.

I have caching enabled for a page and I'm outputting Math.random() after the page title to see if the page comes out of the cache.

So I go to the page, press F5 and I keep seeing the same number in the title. So the page should come out of the cache.

However, sometimes I don't see any data when I go to http://localhost:3000/__nuxt_multi_cache/stats/page.

And even if I call purgeAll, the same number remains on the page, i.e. it should still come from the cache (which, of course, was deleted). But that can't really be?!

Even if I stop and restart the local server (npm run dev) I have the same number on the page. That shouldn't be the case either, right?

Also the enabledForRequest doesn't seem to work properly. I just had another version in the cache where the main navigation of a logged in user was displayed :-(. I just defined it like this:

enabledForRequest: function (req) {
  if (req.headers.cookie) {
    return req.headers.cookie.includes('auth._token.local=');
  }
  return true;
},

@iparker
Copy link
Author

iparker commented Jun 12, 2023

Also I have sometimes errors like this:

image

@dulnan
Copy link
Owner

dulnan commented Jun 12, 2023

And even if I call purgeAll, the same number remains on the page, i.e. it should still come from the cache (which, of course, was deleted). But that can't really be?!

Even if I stop and restart the local server (npm run dev) I have the same number on the page. That shouldn't be the case either, right?

This is basically impossible. V1 of the module is using lru-cache for caching, which is an in-memory cache that only exists as long as the Nuxt app is running. When you stop Nuxt, the cache is gone.

Just out of curiosity: Can you stop the Nuxt server and try to open the page? Is it possible there is another Nuxt process running as well?

Also: Do you use the mode: 'static' option for the route cache? In this case the cached pages are stored on disk as HTML files. But they would need to be served by a web server and Apache/nginx would need to be configured specifically for this to be the case. The module itself won't serve the cached pages from disk.

return req.headers.cookie.includes('auth._token.local=');

I just realized the code I suggested had a bug - sorry! This will return true if the cookie is present. But we actually want it to be false, so that it's not enabled for request:

//     ▼
return !req.headers.cookie.includes('auth._token.local=');

@iparker
Copy link
Author

iparker commented Jun 12, 2023

Thanks again for your feedback and help, and sorry for bothering you with my problems.

This is basically impossible. V1 of the module is using lru-cache for caching, which is an in-memory cache that only exists as long as the Nuxt app is running. When you stop Nuxt, the cache is gone.

Yes, that's what I thought too and I have no explanation for it. I have to discuss this again with my colleagues tomorrow. Currently I could no longer reproduce this behavior after a restart.

Just out of curiosity: Can you stop the Nuxt server and try to open the page? Is it possible there is another Nuxt process running as well?

No, if I stop the server and go to the site, it's not available. When I look for other processes (ps aux | grep 'nuxt') I don't find any other processes.

Also: Do you use the mode: 'static' option for the route cache? In this case the cached pages are stored on disk as HTML files. But they would need to be served by a web server and Apache/nginx would need to be configured specifically for this to be the case. The module itself won't serve the cached pages from disk.

No I do not think so. Which setting would that be? We use several other modules, for example the pwa-module or workbox-caching like this:

workbox: {
  skipWaiting: true,
  runtimeCaching: [
    // Regel für JS und CSS-Dateien
    {
      urlPattern: '.*.(?:js|css)',
      handler: 'CacheFirst',
      method: 'GET',
    },
    // Die bereits bestehende Regel für Bilder
    {
      urlPattern: '.*.(?:png|jpg|jpeg|svg|gif)',
      handler: 'CacheFirst',
      method: 'GET',
    },
  ],
},

But I think rhis should not affect the ssr?

I just realized the code I suggested had a bug - sorry! This will return true if the cookie is present. But we actually want it to be false, so that it's not enabled for request:

Thanks for pointing that out. I have corrected it. Unfortunately the same problems remain.

And I also don't get this: With API-Call stats/page I can find my one cached page. If I do purge/all and then stats/page, I can't find anything anymore. However, it still gives me the same random number on the page so I think the page still comes from cache. Do you have any explanation for this?

Besides that, it somehow seems that the SSR caching of a page doesn't work properly when I press F5 on the first request. It works in such a way that the complete page is not cached? Or can it be that JS errors mean that the page cannot be properly cached? Unfortunately I'm at a loss...

@iparker
Copy link
Author

iparker commented Jun 13, 2023

Hello! I have a quick additional question:

If I understand it correctly, the enabledForRequest controls whether a page from the cache should be displayed for a logged-in user or not.

How can I ensure that a page is not cached for logged-in users, i.e. it cannot happen that a page call by a logged-in user caches a page?

Do I have to do something like this in the page:

if (!app.$auth.loggedIn) {
    app.$multicache.route.setCacheable();
    app.$multicache.route.addTags(['eventjobs_index']);
}

Or does that not make any sense?

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

No branches or pull requests

2 participants