Skip to main content

max / balanced_breakfast

Install the rustls crypto provider at startup reqwest reaches BB through synckit-client and tauri-plugin-updater, both built with `rustls-no-provider`, which makes installing a process-wide provider the consumer app's job. BB never did it, so the first sync or update check would panic inside reqwest's client builder with "No provider set". audiofiles and GoingsOn both install ring at startup; BB was the gap. Installed in `build_app` rather than in `main`, so the mobile entry point is covered by the same line. Twelve sync_service unit tests were failing for the same reason and now pass. synckit-client installs ring under its own `#[cfg(test)]`, which is dead code when it is compiled as our dependency, so the tests that construct a client need their own install; `ensure_crypto_provider` in the shared test module does it once.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-28 02:29 UTC
Signed with PGP, not checked
Commit: a428e60f9351f46304d712cc7ef4436a555b099b
Parent: 7257261
6 files changed, +31 insertions, -0 deletions
M Cargo.lock +1
@@ -451,6 +451,7 @@
451 451 "rand 0.10.2",
452 452 "regex",
453 453 "roxmltree",
454 + "rustls",
454 455 "serde",
455 456 "serde_json",
456 457 "sha2 0.11.0",
@@ -50,6 +50,11 @@
50 50
51 51 # Cloud sync
52 52 synckit-client.workspace = true
53 + # reqwest reaches us through synckit-client and tauri-plugin-updater built with
54 + # `rustls-no-provider`, so the consumer app owes rustls a process-wide crypto
55 + # provider before any TLS client is built. We install ring in `build_app`.
56 + # Matches audiofiles. Trust anchors come from the OS via rustls-platform-verifier.
57 + rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "logging", "ring"] }
53 58 sha2 = "0.11"
54 59 base64.workspace = true
55 60 rand.workspace = true
@@ -20,6 +20,15 @@
20 20
21 21 /// Build the Tauri app with all commands registered.
22 22 pub fn build_app() -> tauri::Builder<tauri::Wry> {
23 + // Install the process-wide rustls crypto provider (ring) before any TLS
24 + // client is built. reqwest reaches us via synckit-client and the updater
25 + // plugin, both compiled with `rustls-no-provider`, so without this the first
26 + // sync or update check panics with "No provider set" inside reqwest's
27 + // client builder. Both entry points (desktop `main`, `mobile_main`) come
28 + // through here, so this is the one place that covers every platform.
29 + // Err means a provider is already installed, which is the outcome we want.
30 + let _ = rustls::crypto::ring::default_provider().install_default();
31 +
23 32 let builder = tauri::Builder::default();
24 33
25 34 // Desktop-only plugins
@@ -683,6 +683,7 @@
683 683 const TEST_KEY: [u8; 32] = [42u8; 32];
684 684
685 685 fn mock_client(server_url: &str) -> SyncKitClient {
686 + ensure_crypto_provider();
686 687 let client = SyncKitClient::new(SyncKitConfig {
687 688 server_url: server_url.to_string(),
688 689 api_key: "test-key".to_string(),
@@ -164,6 +164,20 @@
164 164 use super::*;
165 165 use uuid::Uuid;
166 166
167 + /// Install the rustls crypto provider for tests that build a `SyncKitClient`.
168 + ///
169 + /// `build_app` does this for the real app, but no test runs it. synckit-client
170 + /// installs ring under its own `#[cfg(test)]`, which is dead when it is
171 + /// compiled as our dependency, so every test that constructs a client has to
172 + /// do it here or reqwest's builder panics with "No provider set".
173 + pub(crate) fn ensure_crypto_provider() {
174 + static PROVIDER: std::sync::Once = std::sync::Once::new();
175 + PROVIDER.call_once(|| {
176 + // Err means a provider is already installed, which is the outcome we want.
177 + let _ = rustls::crypto::ring::default_provider().install_default();
178 + });
179 + }
180 +
167 181 pub(crate) async fn setup_test_db() -> SqlitePool {
168 182 let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
169 183 sqlx::migrate!("../migrations/sqlite")
@@ -114,6 +114,7 @@
114 114
115 115 /// Create a SyncKitClient pointing at the mock server with auth and key set.
116 116 fn mock_client(server_url: &str) -> SyncKitClient {
117 + ensure_crypto_provider();
117 118 let client = SyncKitClient::new(SyncKitConfig {
118 119 server_url: server_url.to_string(),
119 120 api_key: "test-key".to_string(),