Update http crate name (#15041)

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki 2024-07-23 15:01:05 -07:00 committed by GitHub
parent d36ebc8c74
commit 855048041d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 184 additions and 180 deletions

View file

@ -0,0 +1,28 @@
[package]
name = "http_client"
version = "0.1.0"
edition = "2021"
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[features]
test-support = []
[lib]
path = "src/http_client.rs"
doctest = true
[dependencies]
http = "1.0.0"
anyhow.workspace = true
derive_more.workspace = true
futures.workspace = true
isahc.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
futures-lite.workspace = true
url.workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

View file

@ -0,0 +1,149 @@
use crate::HttpClient;
use anyhow::{anyhow, bail, Context, Result};
use futures::AsyncReadExt;
use serde::Deserialize;
use std::sync::Arc;
use url::Url;
pub struct GitHubLspBinaryVersion {
pub name: String,
pub url: String,
}
#[derive(Deserialize, Debug)]
pub struct GithubRelease {
pub tag_name: String,
#[serde(rename = "prerelease")]
pub pre_release: bool,
pub assets: Vec<GithubReleaseAsset>,
pub tarball_url: String,
pub zipball_url: String,
}
#[derive(Deserialize, Debug)]
pub struct GithubReleaseAsset {
pub name: String,
pub browser_download_url: String,
}
pub async fn latest_github_release(
repo_name_with_owner: &str,
require_assets: bool,
pre_release: bool,
http: Arc<dyn HttpClient>,
) -> Result<GithubRelease, anyhow::Error> {
let mut response = http
.get(
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases"),
Default::default(),
true,
)
.await
.context("error fetching latest release")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading latest release")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
Ok(releases) => releases,
Err(err) => {
log::error!("Error deserializing: {:?}", err);
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
return Err(anyhow!("error deserializing latest release"));
}
};
releases
.into_iter()
.filter(|release| !require_assets || !release.assets.is_empty())
.find(|release| release.pre_release == pre_release)
.ok_or(anyhow!("Failed to find a release"))
}
pub async fn get_release_by_tag_name(
repo_name_with_owner: &str,
tag: &str,
http: Arc<dyn HttpClient>,
) -> Result<GithubRelease, anyhow::Error> {
let mut response = http
.get(
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
Default::default(),
true,
)
.await
.context("error fetching latest release")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading latest release")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
log::error!("Error deserializing: {:?}", err);
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
anyhow!("error deserializing GitHub release")
})?;
Ok(release)
}
pub fn build_tarball_url(repo_name_with_owner: &str, tag: &str) -> Result<String> {
let mut url = Url::parse(&format!(
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
))?;
// We're pushing this here, because tags may contain `/` and other characters
// that need to be escaped.
let tarball_filename = format!("{}.tar.gz", tag);
url.path_segments_mut()
.map_err(|_| anyhow!("cannot modify url path segments"))?
.push(&tarball_filename);
Ok(url.to_string())
}
#[cfg(test)]
mod tests {
use crate::github::build_tarball_url;
#[test]
fn test_build_tarball_url() {
let tag = "release/2.3.5";
let repo_name_with_owner = "microsoft/vscode-eslint";
let have = build_tarball_url(repo_name_with_owner, tag).unwrap();
assert_eq!(
have,
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
);
}
}

View file

@ -0,0 +1,319 @@
pub mod github;
pub use anyhow::{anyhow, Result};
use derive_more::Deref;
use futures::future::BoxFuture;
use futures_lite::FutureExt;
use isahc::config::{Configurable, RedirectPolicy};
pub use isahc::{
http::{Method, StatusCode, Uri},
AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
};
#[cfg(feature = "test-support")]
use std::fmt;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
pub use url::Url;
pub trait HttpClient: Send + Sync {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
fn get<'a>(
&'a self,
uri: &str,
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
let request = isahc::Request::builder()
.redirect_policy(if follow_redirects {
RedirectPolicy::Follow
} else {
RedirectPolicy::None
})
.method(Method::GET)
.uri(uri)
.body(body);
match request {
Ok(request) => self.send(request),
Err(error) => async move { Err(error.into()) }.boxed(),
}
}
fn post_json<'a>(
&'a self,
uri: &str,
body: AsyncBody,
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
let request = isahc::Request::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.body(body);
match request {
Ok(request) => self.send(request),
Err(error) => async move { Err(error.into()) }.boxed(),
}
}
fn proxy(&self) -> Option<&Uri>;
}
/// An [`HttpClient`] that may have a proxy.
#[derive(Deref)]
pub struct HttpClientWithProxy {
#[deref]
client: Arc<dyn HttpClient>,
proxy: Option<Uri>,
}
impl HttpClientWithProxy {
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
pub fn new(proxy_url: Option<String>) -> Self {
let proxy_url = proxy_url
.and_then(|input| {
input
.parse::<Uri>()
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
.ok()
})
.or_else(read_proxy_from_env);
Self {
client: client(proxy_url.clone()),
proxy: proxy_url,
}
}
}
impl HttpClient for HttpClientWithProxy {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
self.client.send(req)
}
fn proxy(&self) -> Option<&Uri> {
self.proxy.as_ref()
}
}
impl HttpClient for Arc<HttpClientWithProxy> {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
self.client.send(req)
}
fn proxy(&self) -> Option<&Uri> {
self.proxy.as_ref()
}
}
/// An [`HttpClient`] that has a base URL.
pub struct HttpClientWithUrl {
base_url: Mutex<String>,
client: HttpClientWithProxy,
}
impl HttpClientWithUrl {
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
pub fn new(base_url: impl Into<String>, proxy_url: Option<String>) -> Self {
let client = HttpClientWithProxy::new(proxy_url);
Self {
base_url: Mutex::new(base_url.into()),
client,
}
}
/// Returns the base URL.
pub fn base_url(&self) -> String {
self.base_url
.lock()
.map_or_else(|_| Default::default(), |url| url.clone())
}
/// Sets the base URL.
pub fn set_base_url(&self, base_url: impl Into<String>) {
let base_url = base_url.into();
self.base_url
.lock()
.map(|mut url| {
*url = base_url;
})
.ok();
}
/// Builds a URL using the given path.
pub fn build_url(&self, path: &str) -> String {
format!("{}{}", self.base_url(), path)
}
/// Builds a Zed API URL using the given path.
pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
let base_url = self.base_url();
let base_api_url = match base_url.as_ref() {
"https://zed.dev" => "https://api.zed.dev",
"https://staging.zed.dev" => "https://api-staging.zed.dev",
"http://localhost:3000" => "http://localhost:8080",
other => other,
};
Ok(Url::parse_with_params(
&format!("{}{}", base_api_url, path),
query,
)?)
}
}
impl HttpClient for Arc<HttpClientWithUrl> {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
self.client.send(req)
}
fn proxy(&self) -> Option<&Uri> {
self.client.proxy.as_ref()
}
}
impl HttpClient for HttpClientWithUrl {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
self.client.send(req)
}
fn proxy(&self) -> Option<&Uri> {
self.client.proxy.as_ref()
}
}
pub fn client(proxy: Option<Uri>) -> Arc<dyn HttpClient> {
Arc::new(HttpClientWithProxy {
client: Arc::new(
isahc::HttpClient::builder()
.connect_timeout(Duration::from_secs(5))
.low_speed_timeout(100, Duration::from_secs(5))
.proxy(proxy.clone())
.build()
.unwrap(),
),
proxy,
})
}
fn read_proxy_from_env() -> Option<Uri> {
const ENV_VARS: &[&str] = &[
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
];
for var in ENV_VARS {
if let Ok(env) = std::env::var(var) {
return env.parse::<Uri>().ok();
}
}
None
}
impl HttpClient for isahc::HttpClient {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
let client = self.clone();
Box::pin(async move { client.send_async(req).await })
}
fn proxy(&self) -> Option<&Uri> {
None
}
}
#[cfg(feature = "test-support")]
type FakeHttpHandler = Box<
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
+ Send
+ Sync
+ 'static,
>;
#[cfg(feature = "test-support")]
pub struct FakeHttpClient {
handler: FakeHttpHandler,
}
#[cfg(feature = "test-support")]
impl FakeHttpClient {
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
where
Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
{
Arc::new(HttpClientWithUrl {
base_url: Mutex::new("http://test.example".into()),
client: HttpClientWithProxy {
client: Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))),
}),
proxy: None,
},
})
}
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
Self::create(|_| async move {
Ok(Response::builder()
.status(404)
.body(Default::default())
.unwrap())
})
}
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
Self::create(|_| async move {
Ok(Response::builder()
.status(200)
.body(Default::default())
.unwrap())
})
}
}
#[cfg(feature = "test-support")]
impl fmt::Debug for FakeHttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FakeHttpClient").finish()
}
}
#[cfg(feature = "test-support")]
impl HttpClient for FakeHttpClient {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
let future = (self.handler)(req);
Box::pin(async move { future.await.map(Into::into) })
}
fn proxy(&self) -> Option<&Uri> {
None
}
}