Skip to content

Commit d4e3e2e

Browse files
authored
fix(deps): use minreq for native-tls, ureq for rustls to control binary size (#59)
On `main`, both `native-tls` and `rustls` use `ureq` via `ureq/native-tls` and `ureq/rustls`. This PR switches the sync HTTP client so each feature uses the dependency that produces the smallest binary: - **`native-tls`** → `minreq 3.0.0-rc.0` + `https-native-tls`. minreq with system TLS is the smallest option (~540 KB). The feature name change from `native-tls` to `https-native-tls` fixes a compilation failure against minreq 3.x (renamed from 2.x). - **`rustls`** → `ureq 3.3.0` + `ureq/rustls`. ureq's rustls backend uses `ring`; minreq's `https-rustls` uses `aws-lc-rs`, which adds ~1.7 MB to the binary. Both dependencies are now optional and only pulled in by their respective feature. A `compile_error!` is emitted if neither TLS feature is enabled.
1 parent 86309cb commit d4e3e2e

5 files changed

Lines changed: 174 additions & 38 deletions

File tree

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
kind: Dependencies
22
body: |-
3-
Replace ureq 3.3.0 with minreq 3.0.0-rc.0
3+
Use minreq 3.0.0-rc.0 for native-tls and ureq 3.3.0 for rustls
44
5-
Reverts the ureq substitution from v1.1.2. The original compilation failure
6-
with minreq 3.0.0-rc.0 was caused by using the renamed feature incorrectly:
7-
the 2.x `rustls` feature became `https-rustls` in 3.0, and `native-tls`
8-
became `https-native-tls`. Using `https-rustls` builds cleanly against
9-
rustls 0.23.x.
10-
11-
Note: the `rustls` feature binary is ~1.7 MB larger than with ureq because
12-
minreq's `https-rustls` uses rustls's default crypto provider (aws-lc-rs)
13-
rather than ring. This is a minreq API limitation with no workaround on the
14-
dependent side. The default `native-tls` build is unaffected.
5+
The `native-tls` feature now uses `minreq 3.0.0-rc.0` (system TLS), which
6+
produces the smallest binary (~540 KB). The `rustls` feature keeps `ureq
7+
3.3.0` because minreq's rustls backend uses `aws-lc-rs` rather than `ring`,
8+
adding ~1.7 MB. Both dependencies are now optional and only included when
9+
their respective feature is enabled.
1510
time: 2026-04-21T18:13:04.362142000+00:00

Cargo.lock

Lines changed: 36 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,21 @@ readme = "README.md"
1515

1616
[dependencies]
1717
semver = "1.0.27"
18-
minreq = { version = "=3.0.0-rc.0", default-features = false }
18+
# native-tls feature only: minreq + system TLS produces the smallest binary (~540 KB).
19+
# minreq's https-rustls uses aws-lc-rs, which adds ~1.7 MB; ureq is used for rustls instead.
20+
minreq = { version = "=3.0.0-rc.0", default-features = false, optional = true }
21+
# rustls feature only: ureq's rustls uses ring rather than aws-lc-rs, keeping the binary small.
22+
# minreq's https-rustls would add ~1.7 MB due to aws-lc-rs; ureq avoids this.
23+
ureq = { version = "3.3.0", default-features = false, optional = true }
1924
serde_json = { version = "1", default-features = false, features = ["std"] }
2025
reqwest = { version = "0.13.2", optional = true, default-features = false, features = ["rustls"] }
2126

2227
[features]
2328
default = ["native-tls", "do-not-track"]
24-
native-tls = ["minreq/https-native-tls"]
25-
rustls = ["minreq/https-rustls"]
29+
# Uses minreq + system TLS. Smallest binary (~540 KB). Mutually exclusive with `rustls`.
30+
native-tls = ["dep:minreq", "minreq/https-native-tls"]
31+
# Uses ureq + rustls/ring. Pure-Rust TLS, no system dependencies. Mutually exclusive with `native-tls`.
32+
rustls = ["dep:ureq", "ureq/rustls"]
2633
async = ["reqwest"]
2734
do-not-track = []
2835
response-body = []

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ Existing update checker crates add significant binary bloat:
1717
| updates | ~1.8MB |
1818

1919
This crate achieves minimal size by:
20-
- Using `native-tls` (system TLS) instead of bundling rustls
21-
- Minimal JSON parsing (string search instead of serde)
20+
- Using `native-tls` (system TLS via `minreq`) instead of bundling rustls by default
21+
- Using `ureq` for the `rustls` feature, which uses `ring` rather than `aws-lc-rs` (minreq's
22+
rustls backend would add ~1.7 MB due to aws-lc-rs)
2223
- Simple file-based caching
2324

2425
## Installation
@@ -138,9 +139,9 @@ async fn main() {
138139

139140
| Feature | Default | Description |
140141
|---------|---------|-------------|
141-
| `native-tls` || Uses system TLS libraries, smaller binary |
142+
| `native-tls` || System TLS via `minreq`. Smallest binary (~540 KB). |
142143
| `do-not-track` || Respects the `DO_NOT_TRACK` environment variable |
143-
| `rustls` | | Pure Rust TLS, no system dependencies |
144+
| `rustls` | | Pure-Rust TLS via `ureq` + ring. No system dependencies; good for cross-compilation. Uses `ureq` rather than `minreq` to avoid `aws-lc-rs` (~1.7 MB overhead). |
144145
| `async` | | Async support using `reqwest` |
145146
| `response-body` | | Includes the raw crates.io response body in `UpdateInfo` |
146147

src/lib.rs

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@
4040
//!
4141
//! ## Feature Flags
4242
//!
43-
//! - `native-tls` (default): Uses system TLS, smaller binary size
44-
//! - `rustls`: Pure Rust TLS, better for cross-compilation
43+
//! - `native-tls` (default): Uses `minreq` + system TLS. Smallest binary (~540 KB).
44+
//! - `rustls`: Uses `ureq` + rustls/ring. Pure-Rust TLS, no system dependencies.
45+
//! Uses `ureq` rather than `minreq` because `minreq`'s rustls backend pulls in `aws-lc-rs`,
46+
//! adding ~1.7 MB. `ureq`'s rustls uses `ring`, keeping the binary small.
4547
//! - `async`: Enables async support using `reqwest`
4648
//! - `do-not-track` (default): Respects [`DO_NOT_TRACK`] environment variable
4749
//! - `response-body`: Includes the raw crates.io response body in [`DetailedUpdateInfo`]
@@ -90,6 +92,9 @@ use std::fs;
9092
use std::path::PathBuf;
9193
use std::time::{Duration, SystemTime};
9294

95+
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
96+
compile_error!("At least one TLS feature must be enabled: `native-tls` or `rustls`");
97+
9398
pub(crate) const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
9499

95100
const MAX_MESSAGE_SIZE: usize = 4096;
@@ -401,24 +406,53 @@ impl UpdateChecker {
401406
Ok((latest, response_body))
402407
}
403408

409+
/// Build a ureq agent with the configured timeout.
410+
///
411+
/// ureq is used for the `rustls` feature because its rustls backend uses ring
412+
/// rather than aws-lc-rs, avoiding the ~1.7 MB binary size increase that
413+
/// minreq's https-rustls feature would add.
414+
#[cfg(feature = "rustls")]
415+
fn build_ureq_agent(&self) -> ureq::Agent {
416+
ureq::Agent::config_builder()
417+
.timeout_global(Some(self.timeout))
418+
.build()
419+
.into()
420+
}
421+
404422
/// Fetch the latest version from crates.io.
405423
fn fetch_latest_version(&self) -> Result<(String, Option<String>), Error> {
406424
let url = format!("https://crates.io/api/v1/crates/{}", self.crate_name);
407425

408-
let response = minreq::get(&url)
409-
.with_timeout(self.timeout.as_secs())
410-
.with_header("User-Agent", USER_AGENT)
411-
.send()
426+
// rustls uses ureq (ring-based, small binary); native-tls uses minreq (system TLS, smallest binary).
427+
// See Cargo.toml for why the two features use different HTTP clients.
428+
#[cfg(feature = "rustls")]
429+
let body = self
430+
.build_ureq_agent()
431+
.get(&url)
432+
.header("User-Agent", USER_AGENT)
433+
.call()
434+
.map_err(|e| Error::HttpError(e.to_string()))?
435+
.body_mut()
436+
.read_to_string()
412437
.map_err(|e| Error::HttpError(e.to_string()))?;
413438

414-
let body = response
415-
.as_str()
416-
.map_err(|e| Error::HttpError(e.to_string()))?;
439+
#[cfg(not(feature = "rustls"))]
440+
let body = {
441+
let response = minreq::get(&url)
442+
.with_timeout(self.timeout.as_secs())
443+
.with_header("User-Agent", USER_AGENT)
444+
.send()
445+
.map_err(|e| Error::HttpError(e.to_string()))?;
446+
response
447+
.as_str()
448+
.map_err(|e| Error::HttpError(e.to_string()))?
449+
.to_string()
450+
};
417451

418-
let version = extract_newest_version(body)?;
452+
let version = extract_newest_version(&body)?;
419453

420454
#[cfg(feature = "response-body")]
421-
return Ok((version, Some(body.to_string())));
455+
return Ok((version, Some(body)));
422456

423457
#[cfg(not(feature = "response-body"))]
424458
Ok((version, None))
@@ -428,14 +462,29 @@ impl UpdateChecker {
428462
///
429463
/// Best-effort: returns `None` on any failure.
430464
fn fetch_message(&self, url: &str) -> Option<String> {
431-
let response = minreq::get(url)
432-
.with_timeout(self.timeout.as_secs())
433-
.with_header("User-Agent", USER_AGENT)
434-
.send()
465+
// Same client split as fetch_latest_version — see Cargo.toml for rationale.
466+
#[cfg(feature = "rustls")]
467+
let body = self
468+
.build_ureq_agent()
469+
.get(url)
470+
.header("User-Agent", USER_AGENT)
471+
.call()
472+
.ok()?
473+
.body_mut()
474+
.read_to_string()
435475
.ok()?;
436476

437-
let body = response.as_str().ok()?;
438-
truncate_message(body)
477+
#[cfg(not(feature = "rustls"))]
478+
let body = {
479+
let response = minreq::get(url)
480+
.with_timeout(self.timeout.as_secs())
481+
.with_header("User-Agent", USER_AGENT)
482+
.send()
483+
.ok()?;
484+
response.as_str().ok()?.to_string()
485+
};
486+
487+
truncate_message(&body)
439488
}
440489
}
441490

@@ -862,6 +911,57 @@ mod tests {
862911
}
863912
}
864913

914+
// Tests that are specific to the rustls feature (ureq HTTP client path).
915+
// The native-tls path is covered by the tests above, which run with default features.
916+
#[cfg(feature = "rustls")]
917+
mod rustls_tests {
918+
use super::*;
919+
use std::fs;
920+
921+
#[test]
922+
fn builder_works_with_rustls_feature() {
923+
let checker = UpdateChecker::new("test-crate", "1.0.0")
924+
.cache_duration(Duration::from_secs(3600))
925+
.timeout(Duration::from_secs(10));
926+
assert_eq!(checker.crate_name, "test-crate");
927+
assert_eq!(checker.timeout, Duration::from_secs(10));
928+
}
929+
930+
#[test]
931+
fn cache_hit_does_not_invoke_http() {
932+
// Verifies the cache layer works correctly with the ureq path:
933+
// a fresh cache entry must be returned without making any network call.
934+
let dir = tempfile::tempdir().unwrap();
935+
let cache_file = dir.path().join("test-crate-update-check");
936+
fs::write(&cache_file, "99.0.0").unwrap();
937+
938+
let checker = UpdateChecker::new("test-crate", "1.0.0")
939+
.cache_dir(Some(dir.path().to_path_buf()))
940+
.cache_duration(Duration::from_secs(3600));
941+
942+
let result = checker.check().unwrap();
943+
assert!(result.is_some());
944+
assert_eq!(result.unwrap().latest, "99.0.0");
945+
}
946+
947+
#[cfg(feature = "do-not-track")]
948+
#[test]
949+
fn do_not_track_returns_none_with_rustls() {
950+
temp_env::with_var("DO_NOT_TRACK", Some("1"), || {
951+
let checker = UpdateChecker::new("test-crate", "1.0.0").cache_dir(None);
952+
assert!(checker.check().unwrap().is_none());
953+
assert!(checker.check_detailed().unwrap().is_none());
954+
});
955+
}
956+
957+
#[test]
958+
fn invalid_crate_name_rejected_before_http() {
959+
// Ensures validation fires before any HTTP call in the ureq path.
960+
let checker = UpdateChecker::new("", "1.0.0").cache_dir(None);
961+
assert!(matches!(checker.check(), Err(Error::InvalidCrateName(_))));
962+
}
963+
}
964+
865965
#[test]
866966
fn test_message_url_default() {
867967
let checker = UpdateChecker::new("test-crate", "1.0.0");

0 commit comments

Comments
 (0)