fix: self-update poll uses ureq (reqwest has no TLS backend)
All checks were successful
Build / validate (push) Successful in 4m31s

hephd's reqwest client is built default-features=false with no TLS
feature, so the self-update release poll's HTTPS GET always failed
('release check failed: requesting forge releases/latest') — the bug
never surfaced before because nothing in production used reqwest over
HTTPS (hub sync is plain http://). Switch the poll to ureq, which is
already a dependency and ships a rustls/ring TLS stack needing no system
libs (notably no cmake/aws-lc-sys, which would break the rust:bookworm CI
image). Verified end-to-end: a 0.0.0 build now detects v1.1.0, installs,
and restarts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-04 15:26:13 -07:00
commit fac39386d0
3 changed files with 26 additions and 19 deletions

View file

@ -70,19 +70,17 @@ pub fn parse_latest_tag(body: &str) -> Result<String> {
Ok(rel.tag_name)
}
/// Fetch the latest release tag from the forge over HTTP (reusing the daemon's
/// shared `reqwest::Client`). Network/HTTP/JSON failures surface as `Err` for
/// the caller to log-and-continue.
pub async fn fetch_latest_tag(http: &reqwest::Client, url: &str) -> Result<String> {
let body = http
.get(url)
.send()
.await
/// Fetch the latest release tag from the forge over HTTPS, blocking. Uses
/// `ureq` (already a dependency, with a rustls/ring TLS backend that needs no
/// system libs) rather than the daemon's `reqwest` client, which is built
/// without TLS — the forge poll is the only production HTTPS-over-HTTP-client
/// path (hub sync is plain HTTP). Network/HTTP/JSON failures surface as `Err`.
pub fn fetch_latest_tag(url: &str) -> Result<String> {
let body = ureq::get(url)
.call()
.context("requesting forge releases/latest")?
.error_for_status()
.context("forge releases/latest returned an error status")?
.text()
.await
.body_mut()
.read_to_string()
.context("reading forge releases/latest body")?;
parse_latest_tag(&body)
}
@ -93,25 +91,33 @@ pub trait ReleaseSource: Send + Sync + 'static {
fn latest_tag(&self) -> impl std::future::Future<Output = Result<String>> + Send;
}
/// The production source: the forge's `releases/latest` over HTTP.
/// The production source: the forge's `releases/latest` over HTTPS (via `ureq`).
pub struct ForgeReleaseSource {
http: reqwest::Client,
url: String,
}
impl ForgeReleaseSource {
/// Source backed by the daemon's shared client, hitting [`RELEASES_LATEST_URL`].
pub fn new(http: reqwest::Client) -> Self {
/// Source hitting [`RELEASES_LATEST_URL`].
pub fn new() -> Self {
Self {
http,
url: RELEASES_LATEST_URL.to_string(),
}
}
}
impl Default for ForgeReleaseSource {
fn default() -> Self {
Self::new()
}
}
impl ReleaseSource for ForgeReleaseSource {
async fn latest_tag(&self) -> Result<String> {
fetch_latest_tag(&self.http, &self.url).await
// `ureq` is blocking; keep it off the async runtime.
let url = self.url.clone();
tokio::task::spawn_blocking(move || fetch_latest_tag(&url))
.await
.context("release-fetch task panicked")?
}
}

View file

@ -135,7 +135,7 @@ impl Daemon {
let Some(cfg) = self.ctx.self_update.clone() else {
return;
};
let source = selfupdate::ForgeReleaseSource::new(self.ctx.http.clone());
let source = selfupdate::ForgeReleaseSource::new();
let installer: std::sync::Arc<dyn selfupdate::Installer> =
std::sync::Arc::new(selfupdate::CargoInstaller);
let restarter: std::sync::Arc<dyn selfupdate::Restarter> =

View file

@ -0,0 +1 @@
Fix `hephd --self-update` never detecting releases: the release poll used the daemon's `reqwest` client, which is built without a TLS backend (`default-features = false`), so every HTTPS request to the forge failed (`release check failed: requesting forge releases/latest`). The poll now uses `ureq` — already a dependency, with a rustls/ring TLS stack that needs no system libraries (and no cmake/`aws-lc-sys`). Hub sync is unaffected (it is plain HTTP).