Skip to content

feat(updater)!: cache update checks in memory - #3639

Open
beanow-at-crabnebula wants to merge 17 commits into
tauri-apps:v2from
beanow-at-crabnebula:beanow/updater-cache
Open

beanow-at-crabnebula wants to merge 17 commits into
tauri-apps:v2from
beanow-at-crabnebula:beanow/updater-cache

Conversation

@beanow-at-crabnebula

@beanow-at-crabnebula beanow-at-crabnebula commented Sep 24, 2026 •

Copy link
Copy Markdown

Relates to #3641

Update checks now go through an in-memory HTTP cache. Downloads are not cached.

It adds 3 config options to tauri.config.json, see CacheConfig in src/cache.rs.
And individual checks can pick a cache mode: default, bypass or checkNow.
In JS this is check({ cacheMode }), in Rust UpdaterBuilder::cache_mode_override.

The cache follows the server's caching headers.
Responses without max-age or Expires are cached for 1 hour.
No response is cached longer than maxTtlMins, 12 hours by default.
checkNow always contacts the server, but revalidates with a conditional request when it can.

Opt-in first, cache by default later

For this release the cache is opt-in. It will create the struct for an in-memory cache, the middleware, etc.
So the new code will be compiled, just bypass the cache by default.

For a future release the intent is to apply the breaking behavioral change.
Making CacheConfig::disabled_by_default default to false.

The most important action for developers is to set check({ cacheMode: 'checkNow' }) for any user-facing "check now" buttons, where there is clear user-intent to revalidate and get the latest metadata.

For apps relying primarily on "on startup" update checks, this is less important because the cache is in-memory and would be empty on startup.

Testing

Unit tests run against a local HTTP server.
They cover cache lifetimes, the TTL cap, revalidation, each cache mode, shared state and Vary.
The e2e manifests now send caching headers, so update tests also go through the cache.
The API example sets cache options and uses checkNow for its manual check button.

Other BREAKING change

In Rust, network errors from check() are now Error::ReqwestMiddleware(Reqwest(..)) instead of Error::Reqwest. Most apps would not be affected by this, unless they are handling these errors in Rust and match on the particular error variant.

Assisted-by: gpt-6-sol
Assisted-by: gpt-6.1-sol
Assisted-by: claude-opus-5-5

Add a shared in-memory cache for update manifest requests, with plugin configuration for disabling caching and limiting response lifetime. Downloads continue to use their existing request path.

BREAKING CHANGE: Update checks use HTTP caching by default; network errors from check() are wrapped in Error::ReqwestMiddleware; the updater now requires Rust 1.89.0.

Assisted-by: gpt-6-sol
Let Rust and JavaScript callers select the default, bypass, or checkNow policy per update check while retaining the application cache configuration as the default.

Assisted-by: gpt-6-sol
Exercise cache lifetimes, conditional revalidation, per-check modes, shared state, and Vary behavior against a local HTTP server. Make existing updater test manifests cacheable.

Assisted-by: gpt-6-sol
Document the new cache settings, update the Rust version requirement, and let the API example choose how its user-initiated update check uses the cache.

Assisted-by: gpt-6-sol
Hints at the two main places you want to look. (Tauri config and per-request override.)

Also sets values in API example as a way to get manual test coverage of this setting.
@socket-security

socket-security Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​http-cache@​1.0.0-alpha.910010093100100
Addedcargo/​http-cache-quickcache@​1.0.0-alpha.910010093100100
Addedcargo/​http-cache-reqwest@​1.0.0-alpha.109810093100100
Addedcargo/​quick_cache@​0.7.09810093100100
Addedcargo/​reqwest-middleware@​0.5.210010093100100
Updatedcargo/​http@​1.3.1 ⏵ 1.5.010010094 +1100100

View full report

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Package Changes Through 301a33f

There are 4 changes which include opener with patch, opener-js with patch, updater with minor, updater-js with minor

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
api-example 2.0.57 2.0.58
api-example-js 2.0.53 2.0.54
opener 2.7.2 2.7.3
opener-js 2.7.2 2.7.3
updater 2.14.1 2.15.0
updater-js 2.14.1 2.15.0

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

Switches http-cache-reqwest from our git branch to the 1.0.0-alpha.10 release.
Also pins http-cache to 1.0.0-alpha.9 directly.
Adds a test that max_ttl still applies after a checkNow revalidation.

### Why

Upstream released the default_ttl and revalidation patches we were waiting on.
The release also fixes max_ttl being lost after a 304,
which let cached responses outlive maxTtlMins after a checkNow.

http-cache-reqwest's requirement on http-cache allows newer alphas.
Pinning it directly keeps downstream apps on a tested version.

The lockfile moves winapi-util back to windows-sys 0.59, matching v2.
The git dependency had flipped it to 0.48.

The DEFAULT_TTL comment no longer points at the settled upstream issue.

### Testing

The new test fails on the old git branch and passes on the release.
Resolver v3 produces the same lockfile as v2.

Assisted-by: claude-opus-5-5
Merges tauri-apps/plugins-workspace v2 at df59420.

Conflicts resolved:
- Cargo.lock: took upstream's lock and re-resolved for our dependencies.
  http goes from 1.3.1 to 1.5.0, http-cache-semantics needs at least 1.4.
- updater Cargo.toml: kept the tauri "test" feature and our dev-dependencies.
  rust-version follows the workspace again, which is now 1.90.
- updater README: takes the 1.90 MSRV.
- updater.rs: import order only.
- e2e update.rs: uses the new serve_update helper,
  which now responds with the cacheable manifest_response.

The http-cache changeset no longer mentions raising MSRV,
upstream already raised it past 1.89.

### Testing

Updater unit tests, doctests and clippy pass.
The e2e test crate builds, and api-iife.js is unchanged after a rebuild.

Assisted-by: claude-opus-5-5
@beanow-at-crabnebula
beanow-at-crabnebula marked this pull request as ready for review October 23, 2026 12:27

@Legend-Master Legend-Master left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me in general, just some small nitpicks

"json",
"stream",
] }
reqwest-middleware = "0.5.2"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small question here, how much does this increase the final binary size?

@beanow-at-crabnebula beanow-at-crabnebula Oct 27, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the release profile (lto = true, codegen-units = 1, opt-level = "s", panic = "abort")
It seems to add about 304/305 KB, on Linux x86_64.

The majority comes from new crates. And some of it is using more functionality from existing crates.

Rough LLM estimates of the attribution, based on nm results:

Group Crates Δ
HTTP cache logic http_cache 45.4K, http_cache_reqwest 27.3K, http_cache_semantics 22.0K, http_serde 5.4K, postcard 2.2K ~102 KB
Moka in-memory cache moka 58.1K, crossbeam_channel 4.7K, crossbeam_epoch 2.7K, async_lock/event_listener/parking_lot_core 5.1K ~71 KB
Generic std/core code containers, iterators, formatting and drop glue built for the new types ~31 KB
Existing crates used more time 14.0K, http 9.1K, hashbrown 6.2K, futures_util 2.1K, soup 3.1K ~34 KB
Updater plugin's own code tauri_plugin_updater (cache.rs and the new wiring) ~6 KB

The other ~53 KB of the code section isn't tied to any symbol: mostly constant data and strings, plus unwind tables. Other sections add about 11 KB, which is how the stripped binary ends up +305 KB.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you commented specifically on reqwest-middleware.

It is in the data, but it fell under the 1.5 KB cutoff I used to keep the table short. reqwest-middleware costs 1,495 bytes in the size-tuned LTO build and 2,939 bytes in the plain release build.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on how far people are pushing their optimizations that could be significant.

I could look into a cargo feature for this, and that could replace the confusion between disabled and disabled_by_default.

However since we're aiming for default enabled, it would mean people need to use default-features = false. Which may cause it's own friction down the road. Let me know if you want to try this direction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be much easier to understand than the disabled and disabled_by_default configs. Also 300KB seems to be quite significant in terms of how big of an increase in percentage.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, this is what a custom implementation could look like.
beanow-at-crabnebula/plugins-workspace@beanow/updater-cache...beanow-at-crabnebula:plugins-workspace:beanow/updater-cache-lru

Minor improvements from the previous prototype that was measured (body size cap).

Build (Linux x86_64, stripped) vs v2
moka (beanow/updater-cache) +315,616 B
QuickManager prototype +194,512 B
LRU storage +158,016 B
LRU storage + 16 MiB cap (branch head) +158,144 B

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a more complex use-case I wouldn't roll my own, but this seems reasonable to me.
What do you think?

Plus if we do decide that our use-case becomes more complex, switching to the quickmanager option stays a viable path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I don't want to maintain this module ourselves and 30KB sounds ok. Still feel like +200KB is a bit bad for a enabled by default feature. Since we're going with opt-in for now, maybe remove it from the default features?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take that as going with the QuickManager route.

Yes we could take it out of default features. Though I think that comes with a major downside, if people don't discover this feature it means we get no meaningful feedback during the opt-in period to know it's OK to start enabling by default.

At the very least it'd be good to see this compiles and the middleware in Bypass mode is all good.

@beanow-at-crabnebula beanow-at-crabnebula Oct 29, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to quick_cache and took another look at what could be done to shave some bytes off the upstream crates. The only easy win I could find was optional cargo features in quick_cache that http-cache-quickcache doesn't expose: 06chaynes/http-cache#180

Good for around 8912 bytes if we can disable that.

Everything else was significantly more invasive upstream.


Removing reqwest-middleware based on our cache feature does have some saving as well, but would change the public Rust API for Error variants depending on what features you selected. Which may not be worth doing.

We could coerce that into a single variant holding Box<dyn std::error::Error + Send + Sync> so it's the same contract regardless of your feature selection.

Doing that saves roughly 8KB when the cache feature is off.

Build Current PR Prototype* Change
Cache on +202,296 B +199,376 B −2,920 B
Cache off +24,032 B +15,952 B −8,080 B

*= prototype here is a quick LLM attempt at implementing the optional middleware crate and coercing the error variant to be consistent.

middlewest doesn't save anything on its own. With the cache on it saves 1,280 B. With the cache off it adds 1,624 B. Both are noise next to the 200 KB the cache adds. So the reason to switch would be if we want to use it's functionality to cache redirects in their own cache entries.

Comment thread plugins/updater/src/cache.rs Outdated
Comment thread plugins/updater/src/cache.rs Outdated
Comment thread plugins/updater/src/cache.rs Outdated
Renames the cache config `disabled` to `enabled` and `disabledByDefault` to `enabledByDefault`.
The values are inverted, so behavior stays the same: caching is on, but opt-in per request in this release.

### Why

Positive names are easier to read than double negatives like `disabledByDefault = false`.

### Testing

`cargo test -p tauri-plugin-updater --lib` and clippy pass.

Assisted-by: claude-opus-5-5
Replaces the `enabled` cache config with a `cache` cargo feature, on by default.
Without it, `http-cache-reqwest`, `http-cache` and moka are left out of the build.
`check` then never caches, even when a `cacheMode` is passed.

`CacheMode`, `CacheConfig` and `CacheManager` stay public in both builds.
So the JS `cacheMode` option and the `cache` config section still parse.
`CacheManager` no longer implements `Default`.
`UpdaterBuilder::new` now takes the shared one from the plugin state.

### Why

Having both `enabled` and `enabledByDefault` in the config was confusing.
A cargo feature also removes the cache code and dependencies from apps that don't want it.

### Testing

`cargo clippy -p tauri-plugin-updater --all-targets -- -D warnings` and the lib tests pass with default features, with `--all-features`, and with the cache off (`--no-default-features --features rustls-tls,system-proxy,zip`).
A new test checks that without the feature, every request reaches the server.
Linux only.

Assisted-by: claude-opus-5-5
Adds a clippy run and a test run for `tauri-plugin-updater` with `--no-default-features --features rustls-tls,system-proxy,zip`.

### Why

The `cache` feature is on in both the default and `--all-features` runs.
So CI never built the cache-off code path or ran its test.
The test workflow doesn't fail on warnings, so the clippy run catches unused code that only shows up without the feature.

### Testing

Both commands pass locally on Linux.

Assisted-by: claude-opus-5-5
`caches_response_without_caching_headers_for_default_ttl` now gives the 200 and 204 cases a cache each.

### Why

mockito hands out servers from a pool, so the second server can get the same URL as the first.
With a shared cache, the 204 case was then served the cached 200 response and never reached its server.
The test failed every time it ran on its own, and only passed in the full suite.

### Testing

The test now passes 20 out of 20 runs on its own, and in the full suite.

Assisted-by: claude-opus-5-5
"url-standard",
], optional = true }
# Not used directly, pins the version http-cache-reqwest would otherwise let float.
http-cache = { version = "=1.0.0-alpha.9", default-features = false, optional = true }

@beanow-at-crabnebula beanow-at-crabnebula Oct 28, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found possibly two more upstream issues.

  • Revalidation only handles 200 and 304, not 204. This matters because a checkNow followed by default could serve stale results on the default check, when our API promises to update the cache.
  • NoCache doesn't send no-cache on a cache miss.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second one is more nuanced, and not as problematic.

The 204 case though has an open PR: 06chaynes/http-cache#177

Stores cached responses with `http-cache-quickcache`'s `QuickManager` instead of `MokaManager`.
The capacity stays at 6 entries.
`quick_cache` is a direct dependency only to build the cache passed to `QuickManager::new`.

### Why

moka's eviction and TTL machinery is far more than a cache of a few update responses needs.
Freshness is already checked on every read from the stored cache policy, so the storage needs no TTL.
QuickManager keeps us on a maintained upstream manager instead of our own storage.
It also drops `moka`, `crossbeam-epoch`, `portable-atomic` and `tagptr`.

### Testing

Stripped release builds of `tests/app-updater` on Linux x86_64, against `v2` at `df594206`:

| Build                    | Size         | Added    |
| ------------------------ | ------------ | -------- |
| `v2`, no cache           | 4,878,688 B  |          |
| moka                     | 5,202,088 B  | +323,400 |
| QuickManager             | 5,080,984 B  | +202,296 |
| QuickManager, cache off  | 4,902,720 B  | +24,032  |

Clippy and tests pass with all features, and with `--no-default-features --features rustls-tls,system-proxy,zip`.

Assisted-by: claude-opus-5-5
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.

2 participants