From f20a342ecb96734418d0817f841ea14fd9a448e3 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 8 May 2026 21:17:07 +0900 Subject: fix(diagnose): align with Composer's DiagnoseCommand orchestration Restructures diagnose to mirror Composer's 17-step DiagnoseCommand: adds composer.json schema validation, custom composer-repo connectivity, COMPOSER_IPRESOLVE warning, and the checkConnectivityAndComposerNetworkHttpEnablement preflight; drops Mozart-only extras (cache-dir, lock freshness, trailing summary). Extracts the manifest validator into mozart-core::config_validator so both ValidateCommand and DiagnoseCommand depend on the shared module rather than each other -- the same shape Composer uses with Util\\ConfigValidator. Adds a thin HttpDownloader wrapper in mozart-core::http, shadowing Composer's Util\\HttpDownloader. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart-core/src/http.rs | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) (limited to 'crates/mozart-core/src/http.rs') diff --git a/crates/mozart-core/src/http.rs b/crates/mozart-core/src/http.rs index 907b6a7..43de55d 100644 --- a/crates/mozart-core/src/http.rs +++ b/crates/mozart-core/src/http.rs @@ -135,6 +135,84 @@ pub fn default_client() -> reqwest::Client { .expect("failed to build default HTTP client") } +/// Thin wrapper around [`reqwest::Client`] that mirrors the relevant slice of +/// `Composer\Util\HttpDownloader`: a project-shared client used for plain +/// `GET` requests against package metadata URLs. +/// +/// Today this is only the bits the `diagnose` command needs (a pre-built +/// client, a single `get` method, and `exception_hints`). The intention is +/// for `mozart-registry`'s download pipeline to migrate onto the same +/// wrapper later. +#[derive(Clone)] +pub struct HttpDownloader { + client: reqwest::Client, +} + +impl HttpDownloader { + /// Build a downloader using the standard Mozart client (User-Agent + + /// configured root certificates). + pub fn new() -> Self { + Self { + client: default_client(), + } + } + + /// Build a downloader with a custom timeout, used by health checks where + /// hangs would mask the failure mode the user is actually trying to + /// diagnose. + pub fn with_timeout(timeout: std::time::Duration) -> Result { + let client = client_builder() + .timeout(timeout) + .build() + .context("failed to build HTTP client")?; + Ok(Self { client }) + } + + /// Issue a `GET` against `url`. Mirrors `HttpDownloader::get` in role, + /// but returns the raw [`reqwest::Response`] so callers can decide + /// what to do with the body. + pub async fn get(&self, url: &str) -> Result { + self.client.get(url).send().await + } + + /// Underlying client, exposed so callers that need to set additional + /// request-level options can build off it. Try not to use this from + /// new code — prefer extending `HttpDownloader` itself. + pub fn client(&self) -> &reqwest::Client { + &self.client + } +} + +impl Default for HttpDownloader { + fn default() -> Self { + Self::new() + } +} + +/// Mirror of `HttpDownloader::getExceptionHints` from PHP — best-effort +/// human-readable hints for a transport failure. Today this only surfaces +/// the few cases reqwest can distinguish (timeout, connect, decode); we +/// can extend it as we encounter more failure modes in the wild. +pub fn exception_hints(err: &reqwest::Error) -> Vec { + let mut hints = Vec::new(); + if err.is_timeout() { + hints.push( + "The request timed out. Check your network connection or any HTTP proxy settings." + .to_string(), + ); + } + if err.is_connect() { + hints.push( + "Could not establish a connection. Check that the host is reachable and that no firewall is blocking outbound HTTPS." + .to_string(), + ); + } + if err.is_decode() { + hints.push("The response body could not be decoded.".to_string()); + } + hints +} + #[cfg(test)] mod tests { use super::*; -- cgit v1.3.1