Skip to content

breaking: don't abort navigation when calling invalidate(All) during navigation - #16188

Merged
Rich-Harris merged 4 commits into
version-3from
invalidate-navigation-fix
Jul 1, 2026
Merged

breaking: don't abort navigation when calling invalidate(All) during navigation#16188
Rich-Harris merged 4 commits into
version-3from
invalidate-navigation-fix

Conversation

@dummdidumm

Copy link
Copy Markdown
Member

When a navigation starts, invalidate(All) will no longer abort that navigation - previously it indirectly did that because both invalidation and navigation shared the same navigation token; then navigation thought "oh I'm outdated I abort".

Now there's a separate invalidation token so we can handle each case sensibly:

  • when invalidation finishes after goto finishes, it will not apply its stale data. It will apply redirects though since technically it's the last navigation that happened and so it should take precedence
  • when invalidation finishes before goto finishes, it will apply, but it will not abort the navigation

Closes #9354

…g navigation

When a navigation starts, `invalidate(All)` will no longer abort that navigation - previously it indirectly did that because both invalidation and navigation shared the same navigation token; then navigation thought "oh I'm outdated I abort".

Now there's a separate invalidation token so we can handle each case sensibly:
- when invalidation finishes after goto finishes, it will not apply its stale data. It _will_ apply redirects though since technically it's the last navigation that happened and so it should take precedence
- when invalidation finishes before goto finishes, it will apply, but it will not abort the navigation

Closes #9354
@changeset-bot

changeset-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 42ae46b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot

Copy link
Copy Markdown

Comment thread packages/kit/src/runtime/client/client.js
vercel Bot and others added 3 commits June 26, 2026 20:53
…ing a concurrent in-flight `invalidate(All)` to be silently aborted even though the navigation never happened.

This commit fixes the issue reported at packages/kit/src/runtime/client/client.js:1778

## Bug

At the top of `navigate()` in `packages/kit/src/runtime/client/client.js`:

```js
const prev_token = navigation_token;
navigation_token = invalidation_token = nav_token;
```

Both `navigation_token` and `invalidation_token` are overwritten with the new navigation's `nav_token`. However, on the block path (when `beforeNavigate` cancels the navigation and `nav` is `null`), only `navigation_token` is rolled back:

```js
if (!nav) {
    block();
    if (navigation_token === nav_token) navigation_token = prev_token;
    return;
}
```

`invalidation_token` is left stuck at the aborted navigation's `nav_token`, and the previous value was never captured so it couldn't be restored.

### Concrete failure trigger

1. `invalidate(All)` runs `_invalidate()`: sets `token = invalidation_token = A`, captures `nav_token = navigation_token (= N0)`, then `await load_route(intent)`.
2. A navigation is triggered. `navigate()` synchronously runs `navigation_token = invalidation_token = B` (the new `nav_token`).
3. A `beforeNavigate` callback cancels it, so `nav` is `null`. `navigation_token` is restored to `N0`, but `invalidation_token` stays `B`.
4. The invalidation resumes and checks `token !== invalidation_token` → `A !== B` is true → it returns early, silently dropping the invalidation result, even though the navigation never actually happened.

This is a regression from the previous single-shared-`token` design, where the block branch restored the token so an in-flight invalidation was not aborted by a navigation that ended up blocked. The doc comment on `invalidation_token` says it is "Superseeded by both later invalidate(All)s and navigations" — but a *blocked* navigation should not supersede it.

## Fix

Capture the previous `invalidation_token` alongside `prev_token`, and restore it on the block path mirroring the `navigation_token` rollback (guarded by `=== nav_token` so a genuinely later `invalidate(All)` that superseded it is not clobbered):

```js
const prev_token = navigation_token;
const prev_invalidation_token = invalidation_token;
navigation_token = invalidation_token = nav_token;
...
if (!nav) {
    block();
    if (navigation_token === nav_token) navigation_token = prev_token;
    if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
    return;
}
```

The `=== nav_token` guard ensures that if a newer invalidation or navigation has already taken over `invalidation_token` in the meantime, we don't roll it back.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: dummdidumm <sholthausen@web.de>
@Rich-Harris
Rich-Harris merged commit 178eac0 into version-3 Jul 1, 2026
31 of 33 checks passed
@Rich-Harris
Rich-Harris deleted the invalidate-navigation-fix branch July 1, 2026 15:18
Rich-Harris pushed a commit that referenced this pull request Jul 2, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to version-3, this PR
will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

`version-3` is currently in **pre mode** so this branch has prereleases
rather than normal releases. If you want to exit prereleases, run
`changeset pre exit` on `version-3`.

⚠️⚠️⚠️⚠️⚠️⚠️

# Releases
## @sveltejs/adapter-node@6.0.0-next.2

### Major Changes


- breaking: add `kit.paths.origin` config option, remove
`kit.prerender.origin` and the `adapter-node` `ORIGIN` environment
variable ([#16161](#16161))


### Patch Changes

- Updated dependencies
[[`3c434fb`](3c434fb),
[`a9284e8`](a9284e8),
[`3726a7a`](3726a7a),
[`f9d2240`](f9d2240),
[`a9284e8`](a9284e8),
[`7c040ba`](7c040ba),
[`223eaad`](223eaad),
[`223eaad`](223eaad),
[`3b907d4`](3b907d4),
[`fd628a5`](fd628a5),
[`223eaad`](223eaad),
[`6c1d035`](6c1d035),
[`178eac0`](178eac0),
[`c6562a9`](c6562a9),
[`8eca2ab`](8eca2ab),
[`61cf188`](61cf188)]:
  - @sveltejs/kit@3.0.0-next.6
## @sveltejs/kit@3.0.0-next.6

### Major Changes


- breaking: return no content for 204 responses
([#16200](#16200))


- breaking: form action responses now use the HTTP status code returned
from `fail` ([#16200](#16200))


- breaking: nested server-only directories
([#15685](#15685))


- breaking: add `kit.paths.origin` config option, remove
`kit.prerender.origin` and the `adapter-node` `ORIGIN` environment
variable ([#16161](#16161))


- breaking: don't abort navigation when calling `invalidate(All)` during
navigation ([#16188](#16188))


- breaking: allow `handleError` to influence status code
([#16162](#16162))


- breaking: forbid external redirects by default
([#16198](#16198))


### Minor Changes


- feat: use `type: 'module'` for service worker registrations
([#16169](#16169))


- feat: add `dirty()` property to form fields
([#16208](#16208))


- feat: add `cookies.parse` method
([#16203](#16203))


### Patch Changes


- fix: drain unconsumed request bodies so keep-alive connections don't
hang ([#16170](#16170))


- fix: properly handle Date objects in form.fields.set
([#16168](#16168))


- fix: skip clean fields when programmatically validating forms
([#16208](#16208))


- breaking: experimental remote form `validate({ includeUntouched })`
option is now `all`
([#16208](#16208))


- fix: return `undefined` from `fields.branch.issues()` when only
`fields.branch.leaf` has issues
([#16187](#16187))


- feat: add field.touched() helper to remote form fields
([#14692](#14692))
## @sveltejs/adapter-cloudflare@8.0.0-next.1

### Patch Changes


- fix: avoid overriding user's existing `_headers` rules
([#16183](#16183))

- Updated dependencies
[[`3c434fb`](3c434fb),
[`a9284e8`](a9284e8),
[`3726a7a`](3726a7a),
[`f9d2240`](f9d2240),
[`a9284e8`](a9284e8),
[`7c040ba`](7c040ba),
[`223eaad`](223eaad),
[`223eaad`](223eaad),
[`3b907d4`](3b907d4),
[`fd628a5`](fd628a5),
[`223eaad`](223eaad),
[`6c1d035`](6c1d035),
[`178eac0`](178eac0),
[`c6562a9`](c6562a9),
[`8eca2ab`](8eca2ab),
[`61cf188`](61cf188)]:
  - @sveltejs/kit@3.0.0-next.6

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@teemingc teemingc linked an issue Jul 2, 2026 that may be closed by this pull request
@elikoga

elikoga commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

!!! thanks a lot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

invalidateAll() during a navigation causes browser to hang

4 participants