Restore HTTP client transition, but use reqwest everywhere (#19055)
Release Notes: - N/A
This commit is contained in:
parent
c709b66f35
commit
22ac178f9d
35 changed files with 838 additions and 418 deletions
|
@ -26,6 +26,3 @@ serde_json.workspace = true
|
|||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tokio.workspace = true
|
||||
|
|
|
@ -18,6 +18,7 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tls = "0.13"
|
||||
async-tungstenite = { workspace = true, features = ["async-std", "async-tls"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
|
@ -34,8 +35,6 @@ postage.workspace = true
|
|||
rand.workspace = true
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["gpui"] }
|
||||
rustls.workspace = true
|
||||
rustls-native-certs.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
|
|
@ -1023,7 +1023,7 @@ impl Client {
|
|||
&self,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> impl Future<Output = Result<Url>> {
|
||||
) -> impl Future<Output = Result<url::Url>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let url_override = self.rpc_url.read().clone();
|
||||
|
||||
|
@ -1117,7 +1117,7 @@ impl Client {
|
|||
// for us from the RPC URL.
|
||||
//
|
||||
// Among other things, it will generate and set a `Sec-WebSocket-Key` header for us.
|
||||
let mut request = rpc_url.into_client_request()?;
|
||||
let mut request = IntoClientRequest::into_client_request(rpc_url.as_str())?;
|
||||
|
||||
// We then modify the request to add our desired headers.
|
||||
let request_headers = request.headers_mut();
|
||||
|
@ -1137,30 +1137,13 @@ impl Client {
|
|||
|
||||
match url_scheme {
|
||||
Https => {
|
||||
let client_config = {
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
|
||||
let root_certs = rustls_native_certs::load_native_certs();
|
||||
for error in root_certs.errors {
|
||||
log::warn!("error loading native certs: {:?}", error);
|
||||
}
|
||||
root_store.add_parsable_certificates(
|
||||
&root_certs
|
||||
.certs
|
||||
.into_iter()
|
||||
.map(|cert| cert.as_ref().to_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth()
|
||||
};
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_tls::client_async_tls_with_connector(
|
||||
request,
|
||||
stream,
|
||||
Some(client_config.into()),
|
||||
Some(async_tls::TlsConnector::from(
|
||||
http_client::TLS_CONFIG.clone(),
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
Ok(Connection::new(
|
||||
|
|
|
@ -38,7 +38,6 @@ futures.workspace = true
|
|||
google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc_http_client.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
live_kit_server.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -49,6 +48,7 @@ prometheus = "0.13"
|
|||
prost.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
reqwest_client.workspace = true
|
||||
rpc.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
scrypt = "0.11"
|
||||
|
@ -67,7 +67,7 @@ telemetry_events.workspace = true
|
|||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
toml.workspace = true
|
||||
tower = "0.4"
|
||||
tower-http = { workspace = true, features = ["trace"] }
|
||||
|
|
|
@ -23,7 +23,7 @@ use collections::HashMap;
|
|||
use db::TokenUsage;
|
||||
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
|
||||
use futures::{Stream, StreamExt as _};
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use rpc::{
|
||||
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
};
|
||||
|
@ -44,7 +44,7 @@ pub struct LlmState {
|
|||
pub config: Config,
|
||||
pub executor: Executor,
|
||||
pub db: Arc<LlmDatabase>,
|
||||
pub http_client: IsahcHttpClient,
|
||||
pub http_client: ReqwestClient,
|
||||
pub clickhouse_client: Option<clickhouse::Client>,
|
||||
active_user_count_by_model:
|
||||
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
|
||||
|
@ -70,11 +70,8 @@ impl LlmState {
|
|||
let db = Arc::new(db);
|
||||
|
||||
let user_agent = format!("Zed Server/{}", env!("CARGO_PKG_VERSION"));
|
||||
let http_client = IsahcHttpClient::builder()
|
||||
.default_header("User-Agent", user_agent)
|
||||
.build()
|
||||
.map(IsahcHttpClient::from)
|
||||
.context("failed to construct http client")?;
|
||||
let http_client =
|
||||
ReqwestClient::user_agent(&user_agent).context("failed to construct http client")?;
|
||||
|
||||
let this = Self {
|
||||
executor,
|
||||
|
|
|
@ -36,8 +36,8 @@ use collections::{HashMap, HashSet};
|
|||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use core::fmt::{self, Debug, Formatter};
|
||||
use http_client::HttpClient;
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use sha2::Digest;
|
||||
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
|
||||
|
||||
|
@ -961,8 +961,8 @@ impl Server {
|
|||
tracing::info!("connection opened");
|
||||
|
||||
let user_agent = format!("Zed Server/{}", env!("CARGO_PKG_VERSION"));
|
||||
let http_client = match IsahcHttpClient::builder().default_header("User-Agent", user_agent).build() {
|
||||
Ok(http_client) => Arc::new(IsahcHttpClient::from(http_client)),
|
||||
let http_client = match ReqwestClient::user_agent(&user_agent) {
|
||||
Ok(http_client) => Arc::new(http_client),
|
||||
Err(error) => {
|
||||
tracing::error!(?error, "failed to create HTTP client");
|
||||
return;
|
||||
|
|
|
@ -25,7 +25,6 @@ fs.workspace = true
|
|||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc_http_client.workspace = true
|
||||
language.workspace = true
|
||||
languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
|
@ -36,3 +35,4 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
|
|
@ -12,6 +12,7 @@ use language::LanguageRegistry;
|
|||
use node_runtime::NodeRuntime;
|
||||
use open_ai::OpenAiEmbeddingModel;
|
||||
use project::Project;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use semantic_index::{
|
||||
EmbeddingProvider, OpenAiEmbeddingProvider, ProjectIndex, SemanticDb, Status,
|
||||
};
|
||||
|
@ -100,7 +101,7 @@ fn main() -> Result<()> {
|
|||
|
||||
gpui::App::headless().run(move |cx| {
|
||||
let executor = cx.background_executor().clone();
|
||||
let client = isahc_http_client::IsahcHttpClient::new(None, None);
|
||||
let client = Arc::new(ReqwestClient::user_agent("Zed LLM evals").unwrap());
|
||||
cx.set_http_client(client.clone());
|
||||
match cli.command {
|
||||
Commands::Fetch {} => {
|
||||
|
|
|
@ -56,7 +56,6 @@ wit-component.workspace = true
|
|||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
isahc_http_client.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
|
@ -64,5 +63,5 @@ gpui = { workspace = true, features = ["test-support"] }
|
|||
language = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
tokio.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -13,12 +13,12 @@ use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
|||
use gpui::{Context, SemanticVersion, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, DEFAULT_COMPLETION_CONTEXT};
|
||||
use release_channel::AppVersion;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde_json::json;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
|
@ -576,7 +576,8 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
|||
std::env::consts::ARCH
|
||||
)
|
||||
});
|
||||
let builder_client = IsahcHttpClient::new(None, Some(user_agent));
|
||||
let builder_client =
|
||||
Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
|
||||
|
||||
let extension_store = cx.new_model(|cx| {
|
||||
ExtensionStore::new(
|
||||
|
|
|
@ -18,14 +18,14 @@ clap = { workspace = true, features = ["derive"] }
|
|||
env_logger.workspace = true
|
||||
extension = { workspace = true, features = ["no-webrtc"] }
|
||||
fs.workspace = true
|
||||
isahc_http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
theme.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
toml.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
wasmtime.workspace = true
|
||||
|
|
|
@ -13,8 +13,8 @@ use extension::{
|
|||
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
|
||||
ExtensionManifest,
|
||||
};
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use language::LanguageConfig;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use theme::ThemeRegistry;
|
||||
use tree_sitter::{Language, Query, WasmStore};
|
||||
|
||||
|
@ -66,12 +66,7 @@ async fn main() -> Result<()> {
|
|||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
let http_client = Arc::new(
|
||||
IsahcHttpClient::builder()
|
||||
.default_header("User-Agent", user_agent)
|
||||
.build()
|
||||
.map(IsahcHttpClient::from)?,
|
||||
);
|
||||
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?);
|
||||
|
||||
let builder = ExtensionBuilder::new(http_client, scratch_dir);
|
||||
builder
|
||||
|
|
|
@ -1533,4 +1533,8 @@ impl HttpClient for NullHttpClient {
|
|||
fn proxy(&self) -> Option<&http_client::Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -431,7 +431,7 @@ impl TestAppContext {
|
|||
rx
|
||||
}
|
||||
|
||||
/// Retuens a stream of events emitted by the given Model.
|
||||
/// Returns a stream of events emitted by the given Model.
|
||||
pub fn events<Evt, T: 'static + EventEmitter<Evt>>(
|
||||
&mut self,
|
||||
entity: &Model<T>,
|
||||
|
|
|
@ -16,11 +16,13 @@ path = "src/http_client.rs"
|
|||
doctest = true
|
||||
|
||||
[dependencies]
|
||||
http = "0.2"
|
||||
anyhow.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
http = "1.1"
|
||||
log.workspace = true
|
||||
rustls-native-certs.workspace = true
|
||||
rustls.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
|
|
|
@ -11,13 +11,22 @@ use http::request::Builder;
|
|||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
any::type_name,
|
||||
sync::{Arc, LazyLock, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
pub use url::Url;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ReadTimeout(pub Duration);
|
||||
#[derive(Default, Debug, Clone)]
|
||||
impl Default for ReadTimeout {
|
||||
fn default() -> Self {
|
||||
Self(Duration::from_secs(5))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
|
||||
pub enum RedirectPolicy {
|
||||
#[default]
|
||||
NoFollow,
|
||||
|
@ -26,6 +35,23 @@ pub enum RedirectPolicy {
|
|||
}
|
||||
pub struct FollowRedirects(pub bool);
|
||||
|
||||
pub static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| {
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
|
||||
let root_certs = rustls_native_certs::load_native_certs();
|
||||
for error in root_certs.errors {
|
||||
log::warn!("error loading native certs: {:?}", error);
|
||||
}
|
||||
root_store.add_parsable_certificates(&root_certs.certs);
|
||||
|
||||
Arc::new(
|
||||
rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth(),
|
||||
)
|
||||
});
|
||||
|
||||
pub trait HttpRequestExt {
|
||||
/// Set a read timeout on the request.
|
||||
/// For isahc, this is the low_speed_timeout.
|
||||
|
@ -47,6 +73,8 @@ impl HttpRequestExt for http::request::Builder {
|
|||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
|
@ -129,6 +157,10 @@ impl HttpClient for HttpClientWithProxy {
|
|||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for Arc<HttpClientWithProxy> {
|
||||
|
@ -142,6 +174,10 @@ impl HttpClient for Arc<HttpClientWithProxy> {
|
|||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
|
@ -253,6 +289,10 @@ impl HttpClient for Arc<HttpClientWithUrl> {
|
|||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithUrl {
|
||||
|
@ -266,6 +306,10 @@ impl HttpClient for HttpClientWithUrl {
|
|||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_proxy_from_env() -> Option<Uri> {
|
||||
|
@ -306,6 +350,10 @@ impl HttpClient for BlockedHttpClient {
|
|||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
@ -378,4 +426,8 @@ impl HttpClient for FakeHttpClient {
|
|||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "isahc_http_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[lib]
|
||||
path = "src/isahc_http_client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
isahc.workspace = true
|
||||
util.workspace = true
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE-APACHE
|
|
@ -1,105 +0,0 @@
|
|||
use std::{mem, sync::Arc, time::Duration};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use util::maybe;
|
||||
|
||||
pub use isahc::config::Configurable;
|
||||
pub struct IsahcHttpClient(isahc::HttpClient);
|
||||
|
||||
pub use http_client::*;
|
||||
|
||||
impl IsahcHttpClient {
|
||||
pub fn new(proxy: Option<Uri>, user_agent: Option<String>) -> Arc<IsahcHttpClient> {
|
||||
let mut builder = isahc::HttpClient::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.low_speed_timeout(100, Duration::from_secs(5))
|
||||
.proxy(proxy.clone());
|
||||
if let Some(agent) = user_agent {
|
||||
builder = builder.default_header("User-Agent", agent);
|
||||
}
|
||||
Arc::new(IsahcHttpClient(builder.build().unwrap()))
|
||||
}
|
||||
pub fn builder() -> isahc::HttpClientBuilder {
|
||||
isahc::HttpClientBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<isahc::HttpClient> for IsahcHttpClient {
|
||||
fn from(client: isahc::HttpClient) -> Self {
|
||||
Self(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for IsahcHttpClient {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http_client::http::Request<http_client::AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<http_client::Response<http_client::AsyncBody>, anyhow::Error>>
|
||||
{
|
||||
let redirect_policy = req
|
||||
.extensions()
|
||||
.get::<http_client::RedirectPolicy>()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let read_timeout = req
|
||||
.extensions()
|
||||
.get::<http_client::ReadTimeout>()
|
||||
.map(|t| t.0);
|
||||
let req = maybe!({
|
||||
let (mut parts, body) = req.into_parts();
|
||||
let mut builder = isahc::Request::builder()
|
||||
.method(parts.method)
|
||||
.uri(parts.uri)
|
||||
.version(parts.version);
|
||||
if let Some(read_timeout) = read_timeout {
|
||||
builder = builder.low_speed_timeout(100, read_timeout);
|
||||
}
|
||||
|
||||
let headers = builder.headers_mut()?;
|
||||
mem::swap(headers, &mut parts.headers);
|
||||
|
||||
let extensions = builder.extensions_mut()?;
|
||||
mem::swap(extensions, &mut parts.extensions);
|
||||
|
||||
let isahc_body = match body.0 {
|
||||
http_client::Inner::Empty => isahc::AsyncBody::empty(),
|
||||
http_client::Inner::AsyncReader(reader) => isahc::AsyncBody::from_reader(reader),
|
||||
http_client::Inner::SyncReader(reader) => {
|
||||
isahc::AsyncBody::from_bytes_static(reader.into_inner())
|
||||
}
|
||||
};
|
||||
|
||||
builder
|
||||
.redirect_policy(match redirect_policy {
|
||||
http_client::RedirectPolicy::FollowAll => isahc::config::RedirectPolicy::Follow,
|
||||
http_client::RedirectPolicy::FollowLimit(limit) => {
|
||||
isahc::config::RedirectPolicy::Limit(limit)
|
||||
}
|
||||
http_client::RedirectPolicy::NoFollow => isahc::config::RedirectPolicy::None,
|
||||
})
|
||||
.body(isahc_body)
|
||||
.ok()
|
||||
});
|
||||
|
||||
let client = self.0.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
match req {
|
||||
Some(req) => client
|
||||
.send_async(req)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map(|response| {
|
||||
let (parts, body) = response.into_parts();
|
||||
let body = http_client::AsyncBody::from_reader(body);
|
||||
http_client::Response::from_parts(parts, body)
|
||||
}),
|
||||
None => Err(anyhow::anyhow!("Request was malformed")),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ jsonwebtoken.workspace = true
|
|||
log.workspace = true
|
||||
prost.workspace = true
|
||||
prost-types.workspace = true
|
||||
reqwest = "0.11"
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
|
|
34
crates/reqwest_client/Cargo.toml
Normal file
34
crates/reqwest_client/Cargo.toml
Normal file
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[lib]
|
||||
path = "src/reqwest_client.rs"
|
||||
doctest = true
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
path = "examples/client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
bytes = "1.0"
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
serde.workspace = true
|
||||
smol.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
reqwest = { workspace = true, features = ["rustls-tls-manual-roots", "stream"] }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui.workspace = true
|
1
crates/reqwest_client/LICENSE-GPL
Symbolic link
1
crates/reqwest_client/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
41
crates/reqwest_client/examples/client.rs
Normal file
41
crates/reqwest_client/examples/client.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::AsyncReadExt as _;
|
||||
use http_client::AsyncBody;
|
||||
use http_client::HttpClient;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
fn main() {
|
||||
let app = gpui::App::new();
|
||||
app.run(|cx| {
|
||||
cx.spawn(|cx| async move {
|
||||
let client = ReqwestClient::new();
|
||||
let start = Instant::now();
|
||||
let requests = [
|
||||
client.get("https://www.google.com/", AsyncBody::empty(), true),
|
||||
client.get("https://zed.dev/", AsyncBody::empty(), true),
|
||||
client.get("https://docs.rs/", AsyncBody::empty(), true),
|
||||
];
|
||||
let mut requests = requests.into_iter().collect::<FuturesUnordered<_>>();
|
||||
while let Some(response) = requests.next().await {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.unwrap()
|
||||
.into_body()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.unwrap();
|
||||
println!("{}", &body.len());
|
||||
}
|
||||
println!("{:?}", start.elapsed());
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.quit();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
261
crates/reqwest_client/src/reqwest_client.rs
Normal file
261
crates/reqwest_client/src/reqwest_client.rs
Normal file
|
@ -0,0 +1,261 @@
|
|||
use std::{any::type_name, borrow::Cow, io::Read, mem, pin::Pin, sync::OnceLock, task::Poll};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use futures::{AsyncRead, TryStreamExt};
|
||||
use http_client::{http, ReadTimeout, RedirectPolicy};
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
redirect,
|
||||
};
|
||||
use smol::future::FutureExt;
|
||||
|
||||
const DEFAULT_CAPACITY: usize = 4096;
|
||||
|
||||
pub struct ReqwestClient {
|
||||
client: reqwest::Client,
|
||||
proxy: Option<http::Uri>,
|
||||
handle: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
impl ReqwestClient {
|
||||
pub fn new() -> Self {
|
||||
reqwest::Client::new().into()
|
||||
}
|
||||
|
||||
pub fn user_agent(agent: &str) -> anyhow::Result<Self> {
|
||||
let mut map = HeaderMap::new();
|
||||
map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?);
|
||||
let client = reqwest::Client::builder().default_headers(map).build()?;
|
||||
Ok(client.into())
|
||||
}
|
||||
|
||||
pub fn proxy_and_user_agent(proxy: Option<http::Uri>, agent: &str) -> anyhow::Result<Self> {
|
||||
let mut map = HeaderMap::new();
|
||||
map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?);
|
||||
let mut client = reqwest::Client::builder().default_headers(map);
|
||||
if let Some(proxy) = proxy.clone() {
|
||||
client = client.proxy(reqwest::Proxy::all(proxy.to_string())?);
|
||||
}
|
||||
let client = client.build()?;
|
||||
let mut client: ReqwestClient = client.into();
|
||||
client.proxy = proxy;
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
||||
|
||||
impl From<reqwest::Client> for ReqwestClient {
|
||||
fn from(client: reqwest::Client) -> Self {
|
||||
let handle = tokio::runtime::Handle::try_current().unwrap_or_else(|_| {
|
||||
log::info!("no tokio runtime found, creating one for Reqwest...");
|
||||
let runtime = RUNTIME.get_or_init(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
// Since we now have two executors, let's try to keep our footprint small
|
||||
.worker_threads(1)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to initialize HTTP client")
|
||||
});
|
||||
|
||||
runtime.handle().clone()
|
||||
});
|
||||
Self {
|
||||
client,
|
||||
handle,
|
||||
proxy: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This struct is essentially a re-implementation of
|
||||
// https://docs.rs/tokio-util/0.7.12/tokio_util/io/struct.ReaderStream.html
|
||||
// except outside of Tokio's aegis
|
||||
struct StreamReader {
|
||||
reader: Option<Pin<Box<dyn futures::AsyncRead + Send + Sync>>>,
|
||||
buf: BytesMut,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl StreamReader {
|
||||
fn new(reader: Pin<Box<dyn futures::AsyncRead + Send + Sync>>) -> Self {
|
||||
Self {
|
||||
reader: Some(reader),
|
||||
buf: BytesMut::new(),
|
||||
capacity: DEFAULT_CAPACITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures::Stream for StreamReader {
|
||||
type Item = std::io::Result<Bytes>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut();
|
||||
|
||||
let mut reader = match this.reader.take() {
|
||||
Some(r) => r,
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
|
||||
if this.buf.capacity() == 0 {
|
||||
let capacity = this.capacity;
|
||||
this.buf.reserve(capacity);
|
||||
}
|
||||
|
||||
match poll_read_buf(&mut reader, cx, &mut this.buf) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Err(err)) => {
|
||||
self.reader = None;
|
||||
|
||||
Poll::Ready(Some(Err(err)))
|
||||
}
|
||||
Poll::Ready(Ok(0)) => {
|
||||
self.reader = None;
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Ready(Ok(_)) => {
|
||||
let chunk = this.buf.split();
|
||||
self.reader = Some(reader);
|
||||
Poll::Ready(Some(Ok(chunk.freeze())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation from https://docs.rs/tokio-util/0.7.12/src/tokio_util/util/poll_buf.rs.html
|
||||
/// Specialized for this use case
|
||||
pub fn poll_read_buf(
|
||||
io: &mut Pin<Box<dyn futures::AsyncRead + Send + Sync>>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut BytesMut,
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
if !buf.has_remaining_mut() {
|
||||
return Poll::Ready(Ok(0));
|
||||
}
|
||||
|
||||
let n = {
|
||||
let dst = buf.chunk_mut();
|
||||
|
||||
// Safety: `chunk_mut()` returns a `&mut UninitSlice`, and `UninitSlice` is a
|
||||
// transparent wrapper around `[MaybeUninit<u8>]`.
|
||||
let dst = unsafe { &mut *(dst as *mut _ as *mut [std::mem::MaybeUninit<u8>]) };
|
||||
let mut buf = tokio::io::ReadBuf::uninit(dst);
|
||||
let ptr = buf.filled().as_ptr();
|
||||
let unfilled_portion = buf.initialize_unfilled();
|
||||
// SAFETY: Pin projection
|
||||
let io_pin = unsafe { Pin::new_unchecked(io) };
|
||||
std::task::ready!(io_pin.poll_read(cx, unfilled_portion)?);
|
||||
|
||||
// Ensure the pointer does not change from under us
|
||||
assert_eq!(ptr, buf.filled().as_ptr());
|
||||
buf.filled().len()
|
||||
};
|
||||
|
||||
// Safety: This is guaranteed to be the number of initialized (and read)
|
||||
// bytes due to the invariants provided by `ReadBuf::filled`.
|
||||
unsafe {
|
||||
buf.advance_mut(n);
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
|
||||
struct SyncReader {
|
||||
cursor: Option<std::io::Cursor<Cow<'static, [u8]>>>,
|
||||
}
|
||||
|
||||
impl SyncReader {
|
||||
fn new(cursor: std::io::Cursor<Cow<'static, [u8]>>) -> Self {
|
||||
Self {
|
||||
cursor: Some(cursor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures::stream::Stream for SyncReader {
|
||||
type Item = Result<Bytes, std::io::Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let Some(mut cursor) = self.cursor.take() else {
|
||||
return Poll::Ready(None);
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
match cursor.read_to_end(&mut buf) {
|
||||
Ok(_) => {
|
||||
return Poll::Ready(Some(Ok(Bytes::from(buf))));
|
||||
}
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl http_client::HttpClient for ReqwestClient {
|
||||
fn proxy(&self) -> Option<&http::Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<http_client::AsyncBody>,
|
||||
) -> futures::future::BoxFuture<
|
||||
'static,
|
||||
Result<http_client::Response<http_client::AsyncBody>, anyhow::Error>,
|
||||
> {
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
let mut request = self.client.request(parts.method, parts.uri.to_string());
|
||||
request = request.headers(parts.headers);
|
||||
if let Some(redirect_policy) = parts.extensions.get::<RedirectPolicy>() {
|
||||
request = request.redirect_policy(match redirect_policy {
|
||||
RedirectPolicy::NoFollow => redirect::Policy::none(),
|
||||
RedirectPolicy::FollowLimit(limit) => redirect::Policy::limited(*limit as usize),
|
||||
RedirectPolicy::FollowAll => redirect::Policy::limited(100),
|
||||
});
|
||||
}
|
||||
if let Some(ReadTimeout(timeout)) = parts.extensions.get::<ReadTimeout>() {
|
||||
request = request.timeout(*timeout);
|
||||
}
|
||||
let request = request.body(match body.0 {
|
||||
http_client::Inner::Empty => reqwest::Body::default(),
|
||||
http_client::Inner::SyncReader(cursor) => {
|
||||
reqwest::Body::wrap_stream(SyncReader::new(cursor))
|
||||
}
|
||||
http_client::Inner::AsyncReader(stream) => {
|
||||
reqwest::Body::wrap_stream(StreamReader::new(stream))
|
||||
}
|
||||
});
|
||||
|
||||
let handle = self.handle.clone();
|
||||
async move {
|
||||
let mut response = handle.spawn(async { request.send().await }).await??;
|
||||
|
||||
let headers = mem::take(response.headers_mut());
|
||||
let mut builder = http::Response::builder()
|
||||
.status(response.status().as_u16())
|
||||
.version(response.version());
|
||||
*builder.headers_mut().unwrap() = headers;
|
||||
|
||||
let bytes = response
|
||||
.bytes_stream()
|
||||
.map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e))
|
||||
.into_async_read();
|
||||
let body = http_client::AsyncBody::from_reader(bytes);
|
||||
|
||||
builder.body(body).map_err(|e| anyhow!(e))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
|
@ -51,7 +51,6 @@ workspace.workspace = true
|
|||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
isahc_http_client.workspace = true
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
|
@ -62,6 +61,7 @@ language = { workspace = true, features = ["test-support"] }
|
|||
languages.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -2,7 +2,6 @@ use client::Client;
|
|||
use futures::channel::oneshot;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::Project;
|
||||
use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticDb};
|
||||
|
@ -29,7 +28,9 @@ fn main() {
|
|||
let clock = Arc::new(FakeSystemClock::default());
|
||||
|
||||
let http = Arc::new(HttpClientWithUrl::new(
|
||||
IsahcHttpClient::new(None, None),
|
||||
Arc::new(
|
||||
reqwest_client::ReqwestClient::user_agent("Zed semantic index example").unwrap(),
|
||||
),
|
||||
"http://localhost:11434",
|
||||
None,
|
||||
));
|
||||
|
|
|
@ -9,14 +9,14 @@ use thread_local::ThreadLocal;
|
|||
/// Note: this locks on the cloneable sender, but its done once per thread, so it
|
||||
/// shouldn't result in too much contention
|
||||
pub struct UnboundedSyncSender<T: Send> {
|
||||
clonable_sender: Mutex<Sender<T>>,
|
||||
cloneable_sender: Mutex<Sender<T>>,
|
||||
local_senders: ThreadLocal<Sender<T>>,
|
||||
}
|
||||
|
||||
impl<T: Send> UnboundedSyncSender<T> {
|
||||
pub fn new(sender: Sender<T>) -> Self {
|
||||
Self {
|
||||
clonable_sender: Mutex::new(sender),
|
||||
cloneable_sender: Mutex::new(sender),
|
||||
local_senders: ThreadLocal::new(),
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ impl<T: Send> Deref for UnboundedSyncSender<T> {
|
|||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.local_senders
|
||||
.get_or(|| self.clonable_sender.lock().clone())
|
||||
.get_or(|| self.cloneable_sender.lock().clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ editor.workspace = true
|
|||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc_http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
|
@ -36,6 +35,7 @@ strum = { workspace = true, features = ["derive"] }
|
|||
theme.workspace = true
|
||||
title_bar = { workspace = true, features = ["stories"] }
|
||||
ui = { workspace = true, features = ["stories"] }
|
||||
reqwest_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -4,15 +4,17 @@ mod assets;
|
|||
mod stories;
|
||||
mod story_selector;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use dialoguer::FuzzySelect;
|
||||
use gpui::{
|
||||
div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowBounds,
|
||||
WindowOptions,
|
||||
};
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use log::LevelFilter;
|
||||
use project::Project;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use settings::{KeymapFile, Settings};
|
||||
use simplelog::SimpleLogger;
|
||||
use strum::IntoEnumIterator;
|
||||
|
@ -66,8 +68,8 @@ fn main() {
|
|||
gpui::App::new().with_assets(Assets).run(move |cx| {
|
||||
load_embedded_fonts(cx).unwrap();
|
||||
|
||||
let http_client = IsahcHttpClient::new(None, Some("zed_storybook".to_string()));
|
||||
cx.set_http_client(http_client);
|
||||
let http_client = ReqwestClient::user_agent("zed_storybook").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
settings::init(cx);
|
||||
theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
|
||||
|
|
|
@ -729,7 +729,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_trancate_and_trailoff() {
|
||||
fn test_truncate_and_trailoff() {
|
||||
assert_eq!(truncate_and_trailoff("", 5), "");
|
||||
assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè");
|
||||
assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè");
|
||||
|
|
|
@ -17,7 +17,7 @@ neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compat = { version = "0.2.1", "optional" = true }
|
||||
async-compat = { workspace = true, "optional" = true }
|
||||
async-trait = { workspace = true, "optional" = true }
|
||||
collections.workspace = true
|
||||
command_palette.workspace = true
|
||||
|
@ -36,7 +36,7 @@ serde.workspace = true
|
|||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
tokio = { version = "1.15", "optional" = true }
|
||||
tokio = { version = "1.15", features = ["full"], optional = true }
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
|
|
@ -57,7 +57,6 @@ http_client.workspace = true
|
|||
image_viewer.workspace = true
|
||||
inline_completion_button.workspace = true
|
||||
install_cli.workspace = true
|
||||
isahc_http_client.workspace = true
|
||||
journal.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
|
@ -108,6 +107,7 @@ theme.workspace = true
|
|||
theme_selector.workspace = true
|
||||
time.workspace = true
|
||||
ui.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
url.workspace = true
|
||||
urlencoding = "2.1.2"
|
||||
util.workspace = true
|
||||
|
|
|
@ -24,9 +24,9 @@ use gpui::{
|
|||
UpdateGlobal as _, VisualContext,
|
||||
};
|
||||
use http_client::{read_proxy_from_env, Uri};
|
||||
use isahc_http_client::IsahcHttpClient;
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
use assets::Assets;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
|
@ -335,9 +335,7 @@ fn main() {
|
|||
|
||||
log::info!("========== starting zed ==========");
|
||||
|
||||
let app = App::new()
|
||||
.with_assets(Assets)
|
||||
.with_http_client(IsahcHttpClient::new(None, None));
|
||||
let app = App::new().with_assets(Assets);
|
||||
|
||||
let system_id = app.background_executor().block(system_id()).ok();
|
||||
let installation_id = app.background_executor().block(installation_id()).ok();
|
||||
|
@ -471,8 +469,9 @@ fn main() {
|
|||
.ok()
|
||||
})
|
||||
.or_else(read_proxy_from_env);
|
||||
let http = IsahcHttpClient::new(proxy_url, Some(user_agent));
|
||||
cx.set_http_client(http);
|
||||
let http = ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent)
|
||||
.expect("could not start HTTP client");
|
||||
cx.set_http_client(Arc::new(http));
|
||||
|
||||
<dyn Fs>::set_global(fs.clone(), cx);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue