Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Deploying a site breaks it for users who have the site open in their browsers #389

Closed
arggh opened this issue Aug 30, 2018 · 32 comments
Closed

Comments

@arggh
Copy link

arggh commented Aug 30, 2018

I noticed that deploying a Sapper site changes the hashes of all the bundles, so that any navigation, dynamic importing etc. breaks for any currently active users of that site.

The experience for the user is that... basically the site just stops working. He/she clicks a link and nothing happens, except for the title changing to 500 and this being printed at the bottom of the page (where it's highly unlikely to get noticed):

500
Loading chunk 3 failed. (error: https://sapper.app/client/0502fa20d1df95e93ff7/tag_$tag.3.js)

In the console, we get:

Failed to load resource: the server responded with a status of 404 (Not Found)

I get why this is happening, but in my opinion it could be handled a bit more gracefully. Imagine if your site has 10 000 active users simultaneously? I guess one could do rolling deployments so that the old instances are sticky until all the users are dropped off, but that's a tad difficult for most of us.

If it's of any help, GatsbyJS bumped into the same problem here: gatsbyjs/gatsby#4779

@Rich-Harris
Copy link
Member

Oh man, that's a really good point that I hadn't considered. This is definitely a high priority to fix. Thanks

@kylecordes
Copy link

This problem is not unique to Sapper; I've experienced it when deploying React and Angular apps also. To avoid it, something like this works:

  • make your deploy process 's3 sync'/scp/rsync up the new build output, but not delete existing files (i.e hashed files from old versions).
  • run a cron job to delete unchanged files over than N days (where N is big enough noone could possible still have the old version loaded)

@lukeed
Copy link
Member

lukeed commented Sep 17, 2018

This is more of a caching issue, isn't it? When you deploy any site with versioned assets, one would expect that a 30+ day lifespan is fine. Technically, they are immutable, and should even be cached as such. (Most of my projects have a 1Y Cache-Control for versioned assets.

So, I think the real problem is not leveraging a CDN, or using one but purging it between deployments. ZEIT started doing this "as a feature" which is a big mistake IMO.

Proper Cache-Control headers will allow your files to exist for a lifespan of XYZ without requiring that the files actually exist anymore.

@kylecordes
Copy link

Caching won't fix this. A user might load the index page on day 1, then navigate to a new route (and need new assets they never loaded before, thus never cached) on day 5.

(But yes - it is good to mark all the hash-named files as cache-forever-immutable!)

@lukeed
Copy link
Member

lukeed commented Sep 17, 2018

The file(s) wouldn't exist on the user disk, but they should exist on the CDN.

The sapper-template SW is also downloading all routes' entry & chunk files on Day 1, so those assets should already be there (on user disk) for Day 5

@mrkishi
Copy link
Member

mrkishi commented Nov 7, 2018

While using CDNs is great, sapper should probably still attempt a full refresh in case dynamic imports fail with 404s (on navigation only, or else we could potentially get infinite loops in some error conditions).

@njbotkin
Copy link

njbotkin commented Nov 7, 2018

Perhaps on each request, sapper sets a cookie with the current site version? Client triggers a reload if the version changes, no one browses an old site.

@aubergene
Copy link
Contributor

aubergene commented Nov 9, 2018

Please we can update the documentation for export to highlight this issue and advise on best practice for mitigating the issue. I'm happy to work on this. What is the best practice? Ensure that hashed files remain available after each deploy, anything else which needs to be done? What is a good caching strategy?

@nsivertsen
Copy link
Contributor

It's not just a problem with export. I've been having the same problem with the server deployment. Solved it for now (I hope) by forking Sapper and setting cache headers for page routes (https://github.com/sveltejs/sapper/blob/master/templates/src/server/middleware/get_page_handler.ts#L45) to no-cache, no-store, must-revalidate.

See also #415.

@antony
Copy link
Member

antony commented Nov 14, 2018

I think I've just run into exactly the same issue. I was wondering if the service worker could be a possible cause?

My site is simply run using build, not export - but an interesting factor is that the link which broke was one which pointed to an anchor on the page it linked to: https://example.com/some/page#some-anchor - not sure if this is telling or not.

@arggh
Copy link
Author

arggh commented Sep 16, 2019

This is definitely a high priority to fix. Thanks

This was the initial response 1 year ago, and I still think this is a serious problem.

I just wanted to bring this issue to @sw-yx 's attention, as he seems to be the currently active maintainer (?).

@swyxio
Copy link

swyxio commented Sep 16, 2019

not a maintainer. just an interested party trying to get up to speed. i agree this is an important issue but also it seems we can tweak the rollup/webpack bundle ourselves to name chunks? (i havent actually thought this through, just offering userland suggestions). ditto adding headers on routes.

@pngwn
Copy link
Member

pngwn commented Sep 16, 2019

This is due to the old assets not being available in a cached/ currently loaded file, right? In which case, since we use dynamic imports for chunks and such, can't we catch any loading errors on import and force a full reload when they occur?

@arggh
Copy link
Author

arggh commented Sep 16, 2019

@pngwn that would be my proposed solution, but I was thinking maybe the sorcerors here have better ideas that don’t require a full reload. But I’d gladly accept a reload instead of crashing!

@njbotkin
Copy link

Funnily enough, only this morning I set up the reload solution I mentioned earlier in the thread (adding the cookie with Cloudflare workers instead of Sapper, since EVERY request, even CDN-cached requests, must possess the cookie): https://gist.github.com/njbotkin/9a170999e23fb34d4113634a6aba47b0

It's not for everyone, and certainly doesn't resolve this issue, but this is the nicest way I can think of to arbitrarily trigger SPA reloads.

@pngwn
Copy link
Member

pngwn commented Sep 16, 2019

@arggh The only thing that comes to mind is loading the manifest in, so we could grab up to date information from a fresh manifest if a request failed but then what if we just end up getting a cached manifest anyway (since the path to the manifest would need to be constant for this to work)? What if there are substantial changes that render such a manifest completely redundant?

@arggh
Copy link
Author

arggh commented Sep 16, 2019

@pngwn you are right, it's probably not really feasible. HMR in production seems like begging for trouble.

Honestly I haven't given much thought to this, as I'd personally just go with the full reload -solution.

To make it nice, Sapper could provide a way to intercept the reload event, giving a chance to provide a nice UI that explains the situation to the user and lets them manually click "Reload site".

@arggh
Copy link
Author

arggh commented Sep 16, 2019

HMR in production

As a sidenote, Meteor's solution for dynamic imports work pretty much like that: each module is fetched, cached forever until it actually changes, and will not be downloaded again. Updating a single line of code and releasing a new version of the app does not invalidate all modules. Only the changed modules will be re-fetched. They call it "exact code splitting".

@jhwheeler
Copy link

Any update on this? I think the simplest fix would be to have an optional full reload, as per @arggh and @pngwn's solution. Is this or something better in the pipeline?

@arggh
Copy link
Author

arggh commented Nov 15, 2019

I think the simplest fix would be to have an optional full reload, as per @arggh and @pngwn's solution

Just to repeat myself, I'd like to improve that solution by offering a hook for the developer to handle this situation gracefully: offer a dialog to reload the page, for example.

@jhwheeler
Copy link

@arggh Definitely a good idea! For now I will implement this in the 500 page itself.

@lukeed Could you expand on how you would handle the Cache-Control headers? Thanks!

@carcinocron
Copy link

carcinocron commented Nov 24, 2019

I think I have an issue where Firefox Nightly for Android is "hard remembering" my webpack sapper app hosted on Netlify (which claims to have Instant Cache Invalidation). Refreshes still lead to Loading chunk $X failed errors.

Most of the suggestions here are not possible on the mobile version of Firefox, the ones that were possible did not fix the issue: https://stackoverflow.com/questions/41636754/how-to-clear-a-service-worker-cache-in-firefox

I don't know which of 1 or more is causing the issue: FFN, Netlify, Service Workers, Sapper. and it's pretty hard to tell when you don't have the issue outside of the phone.

[Edit] I figured out the cause of my issue. I installed Sentry and discovered that Firefox does not support named capture groups in regexes. Sapper should get an official Sentry plugin.

@wavesforthemasses
Copy link

Hi, I'm trying to find the best approach to deal with this problem.

I'm not a fan of the idea of keeping the old files and deleting them in the future: the app did change and I can't simply wait for the user to hard refresh it in order to get the new version. And I'm not a fan of automatically hard refreshing the user browser while he's using it: what if he's writing something for hours and I destroy all his work?

Plus, correct me if I'm wrong: if I deploy a new version of my app, the user won't experience any problem at all unless he navigates to another page. Right? He can totally continue doing what he's already doing and it'll work properly until he'll navigate to another page.

So, what if I store the current version of my app on my database (I'm using firestore) and if that value changes while someone is using the app, I force a hard refresh BUT only when the user navigate?

So, something like:
$app_v = 7 $: $db_app_v= $db.get("app_version") // realtime $: currentPage && database's app_version != app_version && hardRefresh(currentPage)

Would this work?

Or we can do that, but when we get this error "Failed to load resource: the server responded with a status of 404 (Not Found)". If this error happens we can fetch a JSON file appending a timestamp to query string. On that JSON we store the new version number and if it's different to the current one we hard refresh.

@mikenikles
Copy link

@wavesforthemasses I solve this with a message on the _error.svelte page. It's very generic, but if it's a 500 error, I tell the user there's a new version of the web app available. A button they can click reloads the page and they're now on the latest code.

Source: https://github.com/mikenikles/www-mikenikles-com/blob/master/services/website/src/routes/_error.svelte#L36

@wavesforthemasses
Copy link

@mikenikles in the end I did something similar to your solution using this on that page:

$: process.browser === true && error.message.includes("Failed to fetch dynamically imported module") && !window.location.hash.includes("#forceReload") && window.location.replace(${$path}#forceReload)
It seems to work fine. I'll check your solutions too :) just show a message is not a bad idea.

@ajbouh
Copy link

ajbouh commented Sep 24, 2020

Using the above suggestion worked for me but added an uncomfortable flash before the reload happened.

I'm experimenting with putting this reload logic directly within the navigate method inside the sapper runtime.

@mquandalle
Copy link

Hi, I'm also encountering this issue with a static export of a sapper app on both Vercel and Netlify. Is there any workaround?

It seems to me that if the ressources of the previous deploy aren't available anymore, the navigate function should do a "full request" to the network to fetch the html page and the app bundle instead of rendering the error page locally.

@ajbouh
Copy link

ajbouh commented Feb 12, 2021

I wonder if we should put a sort of token file at a well-known path that is updated to contain a new value on deploy. If we see a failure to load a chunk or other file, we could check the contents of this file and issue a full navigation or page reload if it's changed.

@zolotokrylin
Copy link

We also struggled with this issue. We have found a solution that works for us, though it was a fast patch rather than a thought-through solution. The algo is the following: inside the _error.svelte we have a check if the error string contains the message related to the issue of the outdated files, then we show the screen with the message "New version of the app is available. Clik to reload". The button forces the browser to reload. That's it. Work very well for us. But I think using web workers would be the best way to solve this. When the app is initiated in the browser, web workers need to check if the files are outdated (404?) and if so, request a new version and suggest reloading the app. Google Workbox might help to save time here: https://developers.google.com/web/tools/workbox

@mquandalle
Copy link

mquandalle commented Feb 14, 2021

For future readers, I have been able to solve this issue by doing the following :

Publish a version number

I create a file called _version that I put in the generated _sapper__/export/ directory. This is done in the build script of package.json:

{
"scripts": {
  "build": "sapper export && date '+%s%N' > __sapper__/export/_version"
}

We now have the version number available at https://mysite.com/_version

Update the service worker

We need to update the service worker to disable the cache for this resource. It would be more elegant to rely on an appropriate HTTP header but for now I have hard-coded the bypass logic:

// service-worker.js
self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);
  if (url.pathname === "/_version") {
    return;
  }
  // ...

Detect when a new version is available

In our main layout file, or any other entry file that is included in the application, we create a setInterval function that retrieve the published version number until it detects a new version. The newVersionAvailable variable will be used below when the user click on a link.

// routes/_layout.svelte
<script>
  import { onMount } from "svelte";

  let clientVersion;
  let newVersionAvailable = false;
  onMount(() => {
    const watcher = window.setInterval(async () => {
      if (process.env.NODE_ENV === "development") {
        return;
      }
      const res = await fetch("/_version");
      const serverVersion = await res.text();
      if (serverVersion && !clientVersion) {
        clientVersion = serverVersion;
      } else if (res.ok && serverVersion != clientVersion) {
        newVersionAvailable = true;
        window.clearInterval(watcher);
      }
    }, 10 * 1000);
  });
</script>

Reload the app transparently

We register a global click event to do a full reload of the application when the user click on a link :

<!-- routes/_layout.svelte -->
<svelte:window
  on:click|capture={(event) => {
    if (
      newVersionAvailable &&
      event.target &&
      event.target.href &&
      event.target.target !== "_blank"
    ) {
      event.preventDefault();
      event.stopPropagation();
      window.open(event.target.href, "_parent");
    }
  }}
/>

I haven't have the opportunity to test this logic extensively but so far it seems to works great.

@wavesforthemasses
Copy link

@mquandalle This looks amazing, I'll test it today and give a feedback. I used to run into this issue only while user were actively using my website during an update, but right now I'm facing a new issue on mobile (on a different project): after any update the website breaks unless I empty all the cache. So, I'll see if your solution will fix this. Since it happens to me every time I update the website, it'll be easy to see if it's fixed or not :)

@benmccann
Copy link
Member

Closing as a duplicate of sveltejs/kit#87

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

No branches or pull requests