diff --git a/src/http.rs b/src/http.rs index d331ef49bd..76e84aa1b7 100644 --- a/src/http.rs +++ b/src/http.rs @@ -130,14 +130,25 @@ impl Client { } pub async fn get_text(&self, url: U) -> Result { + self.get_text_with_headers(url, &HeaderMap::new()).await + } + + pub async fn get_text_with_headers( + &self, + url: U, + extra_headers: &HeaderMap, + ) -> Result { let mut url = url.into_url().unwrap(); - let resp = self.get_async(url.clone()).await?; + // Merge GitHub headers with any extra headers provided + let mut headers = github_headers(&url); + headers.extend(extra_headers.clone()); + let resp = self.get_async_with_headers(url.clone(), &headers).await?; let text = resp.text().await?; if text.starts_with("") { if url.scheme() == "http" { // try with https since http may be blocked url.set_scheme("https").unwrap(); - return Box::pin(self.get_text(url)).await; + return Box::pin(self.get_text_with_headers(url, extra_headers)).await; } bail!("Got HTML instead of text from {}", url); } @@ -243,10 +254,22 @@ impl Client { /// POST JSON data to a URL. Returns Ok(true) on success, Ok(false) on non-success status. /// Errors only on network/connection failures. + #[allow(dead_code)] pub async fn post_json( &self, url: U, body: &T, + ) -> Result { + self.post_json_with_headers(url, body, &HeaderMap::new()) + .await + } + + /// POST JSON data to a URL with custom headers. + pub async fn post_json_with_headers( + &self, + url: U, + body: &T, + headers: &HeaderMap, ) -> Result { ensure!(!*env::OFFLINE, "offline mode is enabled"); let url = url.into_url()?; @@ -255,6 +278,7 @@ impl Client { .reqwest .post(url) .header("Content-Type", "application/json") + .headers(headers.clone()) .json(body) .send() .await?; diff --git a/src/versions_host.rs b/src/versions_host.rs index 68b31e4650..aecf129953 100644 --- a/src/versions_host.rs +++ b/src/versions_host.rs @@ -4,6 +4,7 @@ use crate::http; use crate::http::HTTP_FETCH; use crate::plugins::core::CORE_PLUGINS; use crate::registry::REGISTRY; +use reqwest::header::{HeaderMap, HeaderValue}; use std::{ collections::{HashMap, HashSet}, sync::{ @@ -13,6 +14,15 @@ use std::{ }; use tokio::sync::Mutex; +/// Headers for requests to mise-versions, including CI detection +static VERSIONS_HOST_HEADERS: LazyLock = LazyLock::new(|| { + let mut headers = HeaderMap::new(); + if ci_info::is_ci() { + headers.insert("x-mise-ci", HeaderValue::from_static("true")); + } + headers +}); + /// Tools that use the versions host for listing versions /// (excludes java/python due to complex version schemes) static PLUGINS_USE_VERSION_HOST: LazyLock> = LazyLock::new(|| { @@ -68,7 +78,10 @@ pub async fn list_versions(tool: &str) -> eyre::Result>> // Use TOML format which includes created_at timestamps let url = format!("https://mise-versions.jdx.dev/tools/{}.toml", tool); - let versions: Vec = match HTTP_FETCH.get_text(&url).await { + let versions: Vec = match HTTP_FETCH + .get_text_with_headers(&url, &VERSIONS_HOST_HEADERS) + .await + { Ok(body) => { let response: VersionsResponse = toml::from_str(&body)?; response @@ -150,7 +163,10 @@ async fn track_install_async(tool: &str, full: &str, version: &str) -> eyre::Res "arch": *ARCH }); - match HTTP_FETCH.post_json(url, &body).await { + match HTTP_FETCH + .post_json_with_headers(url, &body, &VERSIONS_HOST_HEADERS) + .await + { Ok(true) => trace!("Tracked install: {full}@{version}"), Ok(false) => trace!("Track request failed"), Err(e) => trace!("Track request error: {e}"),