aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/http.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-08 21:17:07 +0900
committernsfisis <nsfisis@gmail.com>2026-05-08 21:17:07 +0900
commitf20a342ecb96734418d0817f841ea14fd9a448e3 (patch)
treefc7c31942579684a13fdb5972216302b1d13eb3a /crates/mozart-core/src/http.rs
parentee17433f9beb95071f37aa6cfe659f14b81ce503 (diff)
downloadphp-mozart-f20a342ecb96734418d0817f841ea14fd9a448e3.tar.gz
php-mozart-f20a342ecb96734418d0817f841ea14fd9a448e3.tar.zst
php-mozart-f20a342ecb96734418d0817f841ea14fd9a448e3.zip
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) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src/http.rs')
-rw-r--r--crates/mozart-core/src/http.rs78
1 files changed, 78 insertions, 0 deletions
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<Self> {
+ 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<reqwest::Response, reqwest::Error> {
+ 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<String> {
+ 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::*;