kit icon indicating copy to clipboard operation
kit copied to clipboard

Proactively handle version updates

Open Rich-Harris opened this issue 3 years ago • 3 comments

Describe the problem

As of #3412, SvelteKit is able to recover from navigations that are broken as a result of an updated version, which is important piece of #87.

It's not perfect though — navigations will 'succeed' if the module for the new page is already cached (either in the module cache already, or cached by a service worker) and any relevant endpoints haven't changed in an incompatible way, but the content could well be stale.

Describe the proposed solution

It would arguably be better if we could do one of two things:

  1. Disable the client-side router as soon as a new version is detected
  2. Load a new route manifest in the background so that we can continue using client-side routing

I don't know whether we'd want to always do 1 or 2, or offer developers a means of selecting between them.

If navigation occurred after a new version was deployed but before it was detected, then we'd still use the current behaviour (checking for an update when an error occurs), but if 2 was enabled then we'd presumably attempt to do a client-side navigation with the updated manifest, rather than immediately falling back to a full page navigation.

Updating the manifest behind the scenes adds some complexity. Changes to appDir or src/app.html would necessitate a full page reload (other config changes would be reflected in hashes, so would be accounted for), so we'd need to a) treat a 404 on ${appDir}/version.json as fatal, and b) include a hash of the template in version.json alongside the version string and a pointer to the manifest.

Similarly, new versions of SvelteKit could make it impossible to update gracefully, so if app version B was built with a different Kit version than app version A, we should force a reload. In fact, it's probably a good idea to have a sort of killswitch mechanism for forcing the reload behaviour when you deploy a new version.

Since 2 sounds exhausting, we might want to only do 1 (but perhaps leave open the possibility for 2 in future).

Alternatives considered

We don't need to do this, it's arguably more of a nice-to-have than anything. The current behaviour captures the most common cases, I think.

Importance

nice to have

Additional Information

No response

Rich-Harris avatar Feb 01 '22 20:02 Rich-Harris

@Rich-Harris Maybe version.json should be moved out of appDir? Now it gets cached in every CDN and browser, public,max-age=31536000,immutable.

bjon avatar Feb 10 '22 22:02 bjon

I just saw that you're requesting the file with pragma: 'no-cache' and 'cache-control': 'no-cache'. If that's 100% safe everything is fine.

bjon avatar Feb 10 '22 22:02 bjon

Not sure if this warrants a separate issue, but it seems that the path to version.json is not considering the assets path setting. Let's say paths.assets is https://example.com/assets, it would then still attempt to load https://example.com/_app/version.json (instead of https://example.com/assets/_app/version.json)

bummzack avatar Feb 16 '22 19:02 bummzack

We have customers running the SvelteKit client 24x7 in their browser, so we are looking for a solution for this. Our challenge is that we try to do zero downtime deployments, so our backend is rolled over to a new version over multiple minutes. Because of round-robin load balancers in front of bare metal servers the user might end up on a different version (old/new) with every request going towards the backend.

bluepuma77 avatar Aug 31 '22 12:08 bluepuma77

It occurs to me that you could implement option 1 with no changes:

<!-- src/routes/+layout.svelte -->
<script>
  import { beforeNavigate } from '$app/navigation';
  import { updated } from '$app/stores';

  beforeNavigate(({ to, cancel }) => {
    if ($updated) {
      cancel();
      location = to;
    }
  });
</script>

On the back end, you could prevent the user from loading data if they're on a stale version by setting a cookie:

export function handle({ event, resolve }) {
  const accept = event.request.headers.get('accept');
  const cookies = cookie.parse(event.request.headers.get('cookie') ?? '');
  if (accept === '*/*' && cookies.app_version = !== CURRENT_VERSION) {
    // this is a `fetch` request from an older version of the app
    return new Response('get with the times grandad', { status: 500 });
  }

  const response = await resolve(event);
  response.headers.append('set-cookie', `app_version=${CURRENT_VERSION}; httpOnly`);
}

Option 2 is excessively complicated and almost guaranteed to fail in interesting ways (and is also memory leak prone).

In light of all this I'm not sure there's much SvelteKit needs to do (except perhaps exposing config.kit.version.name via $app/environment or whatever), so I'll close this issue.

Rich-Harris avatar Sep 03 '22 18:09 Rich-Harris