Stop leaking isahc assumption (#18408)

Conrad Irwin created

Users of our http_client crate knew they were interacting with isahc as
they set its extensions on the request. This change adds our own
equivalents for their APIs in preparation for changing the default http
client.

Release Notes:

- N/A

Change summary

Cargo.lock                                             |  8 -
crates/anthropic/Cargo.toml                            |  1 
crates/anthropic/src/anthropic.rs                      |  7 
crates/copilot/Cargo.toml                              |  1 
crates/copilot/src/copilot_chat.rs                     |  7 
crates/extension/Cargo.toml                            |  1 
crates/extension/src/extension_store.rs                |  2 
crates/extension/src/wasm_host/wit/since_v0_1_0.rs     | 13 
crates/extension/src/wasm_host/wit/since_v0_2_0.rs     | 13 
crates/feedback/Cargo.toml                             |  1 
crates/feedback/src/feedback_modal.rs                  |  3 
crates/git_hosting_providers/src/providers/codeberg.rs |  8 
crates/git_hosting_providers/src/providers/github.rs   |  8 
crates/google_ai/Cargo.toml                            |  1 
crates/google_ai/src/google_ai.rs                      |  7 
crates/gpui/src/app.rs                                 |  3 
crates/http_client/src/http_client.rs                  | 90 ++++++-----
crates/isahc_http_client/src/isahc_http_client.rs      | 26 ++
crates/language_model/Cargo.toml                       |  1 
crates/language_model/src/provider/cloud.rs            |  5 
crates/open_ai/Cargo.toml                              |  1 
crates/open_ai/src/open_ai.rs                          |  7 
crates/zed/Cargo.toml                                  |  1 
crates/zed/src/reliability.rs                          |  5 
24 files changed, 114 insertions(+), 106 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -245,7 +245,6 @@ dependencies = [
  "chrono",
  "futures 0.3.30",
  "http_client",
- "isahc",
  "schemars",
  "serde",
  "serde_json",
@@ -2850,7 +2849,6 @@ dependencies = [
  "gpui",
  "http_client",
  "indoc",
- "isahc",
  "language",
  "lsp",
  "menu",
@@ -4128,7 +4126,6 @@ dependencies = [
  "gpui",
  "http_client",
  "indexed_docs",
- "isahc",
  "isahc_http_client",
  "language",
  "log",
@@ -4289,7 +4286,6 @@ dependencies = [
  "gpui",
  "http_client",
  "human_bytes",
- "isahc",
  "language",
  "log",
  "menu",
@@ -5016,7 +5012,6 @@ dependencies = [
  "anyhow",
  "futures 0.3.30",
  "http_client",
- "isahc",
  "schemars",
  "serde",
  "serde_json",
@@ -6288,7 +6283,6 @@ dependencies = [
  "http_client",
  "image",
  "inline_completion_button",
- "isahc",
  "language",
  "log",
  "menu",
@@ -7591,7 +7585,6 @@ dependencies = [
  "anyhow",
  "futures 0.3.30",
  "http_client",
- "isahc",
  "schemars",
  "serde",
  "serde_json",
@@ -14435,7 +14428,6 @@ dependencies = [
  "image_viewer",
  "inline_completion_button",
  "install_cli",
- "isahc",
  "isahc_http_client",
  "journal",
  "language",

crates/anthropic/Cargo.toml 🔗

@@ -20,7 +20,6 @@ anyhow.workspace = true
 chrono.workspace = true
 futures.workspace = true
 http_client.workspace = true
-isahc.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true

crates/anthropic/src/anthropic.rs 🔗

@@ -6,9 +6,8 @@ use std::{pin::Pin, str::FromStr};
 use anyhow::{anyhow, Context, Result};
 use chrono::{DateTime, Utc};
 use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
-use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
-use isahc::config::Configurable;
-use isahc::http::{HeaderMap, HeaderValue};
+use http_client::http::{HeaderMap, HeaderValue};
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
 use serde::{Deserialize, Serialize};
 use strum::{EnumIter, EnumString};
 use thiserror::Error;
@@ -289,7 +288,7 @@ pub async fn stream_completion_with_rate_limit_info(
         .header("X-Api-Key", api_key)
         .header("Content-Type", "application/json");
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     }
     let serialized_request =
         serde_json::to_string(&request).context("failed to serialize request")?;

crates/copilot/Cargo.toml 🔗

@@ -37,7 +37,6 @@ fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
 http_client.workspace = true
-isahc.workspace = true
 language.workspace = true
 lsp.workspace = true
 menu.workspace = true

crates/copilot/src/copilot_chat.rs 🔗

@@ -7,8 +7,7 @@ use chrono::DateTime;
 use fs::Fs;
 use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Global};
-use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
-use isahc::config::Configurable;
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
 use paths::home_dir;
 use serde::{Deserialize, Serialize};
 use settings::watch_config_file;
@@ -275,7 +274,7 @@ async fn request_api_token(
         .header("Accept", "application/json");
 
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     }
 
     let request = request_builder.body(AsyncBody::empty())?;
@@ -332,7 +331,7 @@ async fn stream_completion(
         .header("Copilot-Integration-Id", "vscode-chat");
 
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     }
     let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
     let mut response = client.send(request).await?;

crates/extension/Cargo.toml 🔗

@@ -28,7 +28,6 @@ futures.workspace = true
 gpui.workspace = true
 http_client.workspace = true
 indexed_docs.workspace = true
-isahc.workspace = true
 language.workspace = true
 log.workspace = true
 lsp.workspace = true

crates/extension/src/extension_store.rs 🔗

@@ -664,7 +664,7 @@ impl ExtensionStore {
 
             let content_length = response
                 .headers()
-                .get(isahc::http::header::CONTENT_LENGTH)
+                .get(http_client::http::header::CONTENT_LENGTH)
                 .and_then(|value| value.to_str().ok()?.parse::<usize>().ok());
 
             let mut body = BufReader::new(response.body_mut());

crates/extension/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -1,5 +1,5 @@
 use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
-use ::http_client::AsyncBody;
+use ::http_client::{AsyncBody, HttpRequestExt};
 use ::settings::{Settings, WorktreeId};
 use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
@@ -8,7 +8,6 @@ use async_trait::async_trait;
 use futures::{io::BufReader, FutureExt as _};
 use futures::{lock::Mutex, AsyncReadExt};
 use indexed_docs::IndexedDocsDatabase;
-use isahc::config::{Configurable, RedirectPolicy};
 use language::{
     language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
 };
@@ -297,10 +296,12 @@ fn convert_request(
     let mut request = ::http_client::Request::builder()
         .method(::http_client::Method::from(extension_request.method))
         .uri(&extension_request.url)
-        .redirect_policy(match extension_request.redirect_policy {
-            http_client::RedirectPolicy::NoFollow => RedirectPolicy::None,
-            http_client::RedirectPolicy::FollowLimit(limit) => RedirectPolicy::Limit(limit),
-            http_client::RedirectPolicy::FollowAll => RedirectPolicy::Follow,
+        .follow_redirects(match extension_request.redirect_policy {
+            http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
+            http_client::RedirectPolicy::FollowLimit(limit) => {
+                ::http_client::RedirectPolicy::FollowLimit(limit)
+            }
+            http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
         });
     for (key, value) in &extension_request.headers {
         request = request.header(key, value);

crates/extension/src/wasm_host/wit/since_v0_2_0.rs 🔗

@@ -1,5 +1,5 @@
 use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
-use ::http_client::AsyncBody;
+use ::http_client::{AsyncBody, HttpRequestExt};
 use ::settings::{Settings, WorktreeId};
 use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
@@ -8,7 +8,6 @@ use async_trait::async_trait;
 use futures::{io::BufReader, FutureExt as _};
 use futures::{lock::Mutex, AsyncReadExt};
 use indexed_docs::IndexedDocsDatabase;
-use isahc::config::{Configurable, RedirectPolicy};
 use language::{
     language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
 };
@@ -213,10 +212,12 @@ fn convert_request(
     let mut request = ::http_client::Request::builder()
         .method(::http_client::Method::from(extension_request.method))
         .uri(&extension_request.url)
-        .redirect_policy(match extension_request.redirect_policy {
-            http_client::RedirectPolicy::NoFollow => RedirectPolicy::None,
-            http_client::RedirectPolicy::FollowLimit(limit) => RedirectPolicy::Limit(limit),
-            http_client::RedirectPolicy::FollowAll => RedirectPolicy::Follow,
+        .follow_redirects(match extension_request.redirect_policy {
+            http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
+            http_client::RedirectPolicy::FollowLimit(limit) => {
+                ::http_client::RedirectPolicy::FollowLimit(limit)
+            }
+            http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
         });
     for (key, value) in &extension_request.headers {
         request = request.header(key, value);

crates/feedback/Cargo.toml 🔗

@@ -23,7 +23,6 @@ editor.workspace = true
 futures.workspace = true
 gpui.workspace = true
 human_bytes = "0.4.1"
-isahc.workspace = true
 http_client.workspace = true
 language.workspace = true
 log.workspace = true

crates/feedback/src/feedback_modal.rs 🔗

@@ -11,7 +11,6 @@ use gpui::{
     PromptLevel, Render, Task, View, ViewContext,
 };
 use http_client::HttpClient;
-use isahc::Request;
 use language::Buffer;
 use project::Project;
 use regex::Regex;
@@ -299,7 +298,7 @@ impl FeedbackModal {
             is_staff: is_staff.unwrap_or(false),
         };
         let json_bytes = serde_json::to_vec(&request)?;
-        let request = Request::post(feedback_endpoint)
+        let request = http_client::http::Request::post(feedback_endpoint)
             .header("content-type", "application/json")
             .body(json_bytes.into())?;
         let mut response = http_client.send(request).await?;

crates/git_hosting_providers/src/providers/codeberg.rs 🔗

@@ -3,7 +3,7 @@ use std::sync::Arc;
 use anyhow::{bail, Context, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
-use http_client::{AsyncBody, HttpClient, Request};
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
 use serde::Deserialize;
 use url::Url;
 
@@ -49,14 +49,16 @@ impl Codeberg {
         let url =
             format!("https://codeberg.org/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}");
 
-        let mut request = Request::get(&url).header("Content-Type", "application/json");
+        let mut request = Request::get(&url)
+            .header("Content-Type", "application/json")
+            .follow_redirects(http_client::RedirectPolicy::FollowAll);
 
         if let Ok(codeberg_token) = std::env::var("CODEBERG_TOKEN") {
             request = request.header("Authorization", format!("Bearer {}", codeberg_token));
         }
 
         let mut response = client
-            .send_with_redirect_policy(request.body(AsyncBody::default())?, true)
+            .send(request.body(AsyncBody::default())?)
             .await
             .with_context(|| format!("error fetching Codeberg commit details at {:?}", url))?;
 

crates/git_hosting_providers/src/providers/github.rs 🔗

@@ -3,7 +3,7 @@ use std::sync::{Arc, OnceLock};
 use anyhow::{bail, Context, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
-use http_client::{AsyncBody, HttpClient, Request};
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
 use regex::Regex;
 use serde::Deserialize;
 use url::Url;
@@ -53,14 +53,16 @@ impl Github {
     ) -> Result<Option<User>> {
         let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}");
 
-        let mut request = Request::get(&url).header("Content-Type", "application/json");
+        let mut request = Request::get(&url)
+            .header("Content-Type", "application/json")
+            .follow_redirects(http_client::RedirectPolicy::FollowAll);
 
         if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
             request = request.header("Authorization", format!("Bearer {}", github_token));
         }
 
         let mut response = client
-            .send_with_redirect_policy(request.body(AsyncBody::default())?, true)
+            .send(request.body(AsyncBody::default())?)
             .await
             .with_context(|| format!("error fetching GitHub commit details at {:?}", url))?;
 

crates/google_ai/Cargo.toml 🔗

@@ -18,7 +18,6 @@ schemars = ["dep:schemars"]
 anyhow.workspace = true
 futures.workspace = true
 http_client.workspace = true
-isahc.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true

crates/google_ai/src/google_ai.rs 🔗

@@ -2,8 +2,7 @@ mod supported_countries;
 
 use anyhow::{anyhow, Result};
 use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
-use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
-use isahc::config::Configurable;
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
 
@@ -30,7 +29,7 @@ pub async fn stream_generate_content(
         .header("Content-Type", "application/json");
 
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     };
 
     let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
@@ -85,7 +84,7 @@ pub async fn count_tokens(
         .header("Content-Type", "application/json");
 
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     }
 
     let http_request = request_builder.body(AsyncBody::from(request))?;

crates/gpui/src/app.rs 🔗

@@ -1524,10 +1524,9 @@ pub struct KeystrokeEvent {
 struct NullHttpClient;
 
 impl HttpClient for NullHttpClient {
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         _req: http_client::Request<http_client::AsyncBody>,
-        _follow_redirects: bool,
     ) -> futures::future::BoxFuture<
         'static,
         Result<http_client::Response<http_client::AsyncBody>, anyhow::Error>,

crates/http_client/src/http_client.rs 🔗

@@ -10,22 +10,46 @@ use futures::future::BoxFuture;
 use http::request::Builder;
 #[cfg(feature = "test-support")]
 use std::fmt;
-use std::sync::{Arc, Mutex};
+use std::{
+    sync::{Arc, Mutex},
+    time::Duration,
+};
 pub use url::Url;
 
+pub struct ReadTimeout(pub Duration);
+#[derive(Default, Debug, Clone)]
+pub enum RedirectPolicy {
+    #[default]
+    NoFollow,
+    FollowLimit(u32),
+    FollowAll,
+}
+pub struct FollowRedirects(pub bool);
+
+pub trait HttpRequestExt {
+    /// Set a read timeout on the request.
+    /// For isahc, this is the low_speed_timeout.
+    /// For other clients, this is the timeout used for read calls when reading the response.
+    /// In all cases this prevents servers stalling completely, but allows them to send data slowly.
+    fn read_timeout(self, timeout: Duration) -> Self;
+    /// Whether or not to follow redirects
+    fn follow_redirects(self, follow: RedirectPolicy) -> Self;
+}
+
+impl HttpRequestExt for http::request::Builder {
+    fn read_timeout(self, timeout: Duration) -> Self {
+        self.extension(ReadTimeout(timeout))
+    }
+
+    fn follow_redirects(self, follow: RedirectPolicy) -> Self {
+        self.extension(follow)
+    }
+}
+
 pub trait HttpClient: 'static + Send + Sync {
     fn send(
         &self,
         req: http::Request<AsyncBody>,
-    ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
-        self.send_with_redirect_policy(req, false)
-    }
-
-    // TODO: Make a better API for this
-    fn send_with_redirect_policy(
-        &self,
-        req: Request<AsyncBody>,
-        follow_redirects: bool,
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>>;
 
     fn get<'a>(
@@ -34,14 +58,17 @@ pub trait HttpClient: 'static + Send + Sync {
         body: AsyncBody,
         follow_redirects: bool,
     ) -> BoxFuture<'a, Result<Response<AsyncBody>, anyhow::Error>> {
-        let request = Builder::new().uri(uri).body(body);
+        let request = Builder::new()
+            .uri(uri)
+            .follow_redirects(if follow_redirects {
+                RedirectPolicy::FollowAll
+            } else {
+                RedirectPolicy::NoFollow
+            })
+            .body(body);
 
         match request {
-            Ok(request) => Box::pin(async move {
-                self.send_with_redirect_policy(request, follow_redirects)
-                    .await
-                    .map_err(Into::into)
-            }),
+            Ok(request) => Box::pin(async move { self.send(request).await.map_err(Into::into) }),
             Err(e) => Box::pin(async move { Err(e.into()) }),
         }
     }
@@ -92,12 +119,11 @@ impl HttpClientWithProxy {
 }
 
 impl HttpClient for HttpClientWithProxy {
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         req: Request<AsyncBody>,
-        follow_redirects: bool,
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
-        self.client.send_with_redirect_policy(req, follow_redirects)
+        self.client.send(req)
     }
 
     fn proxy(&self) -> Option<&Uri> {
@@ -106,12 +132,11 @@ impl HttpClient for HttpClientWithProxy {
 }
 
 impl HttpClient for Arc<HttpClientWithProxy> {
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         req: Request<AsyncBody>,
-        follow_redirects: bool,
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
-        self.client.send_with_redirect_policy(req, follow_redirects)
+        self.client.send(req)
     }
 
     fn proxy(&self) -> Option<&Uri> {
@@ -218,12 +243,11 @@ impl HttpClientWithUrl {
 }
 
 impl HttpClient for Arc<HttpClientWithUrl> {
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         req: Request<AsyncBody>,
-        follow_redirects: bool,
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
-        self.client.send_with_redirect_policy(req, follow_redirects)
+        self.client.send(req)
     }
 
     fn proxy(&self) -> Option<&Uri> {
@@ -232,12 +256,11 @@ impl HttpClient for Arc<HttpClientWithUrl> {
 }
 
 impl HttpClient for HttpClientWithUrl {
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         req: Request<AsyncBody>,
-        follow_redirects: bool,
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
-        self.client.send_with_redirect_policy(req, follow_redirects)
+        self.client.send(req)
     }
 
     fn proxy(&self) -> Option<&Uri> {
@@ -283,14 +306,6 @@ impl HttpClient for BlockedHttpClient {
     fn proxy(&self) -> Option<&Uri> {
         None
     }
-
-    fn send_with_redirect_policy(
-        &self,
-        req: Request<AsyncBody>,
-        _: bool,
-    ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
-        self.send(req)
-    }
 }
 
 #[cfg(feature = "test-support")]
@@ -352,10 +367,9 @@ impl fmt::Debug for FakeHttpClient {
 
 #[cfg(feature = "test-support")]
 impl HttpClient for FakeHttpClient {
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         req: Request<AsyncBody>,
-        _follow_redirects: bool,
     ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
         let future = (self.handler)(req);
         future

crates/isahc_http_client/src/isahc_http_client.rs 🔗

@@ -1,7 +1,6 @@
 use std::{mem, sync::Arc, time::Duration};
 
 use futures::future::BoxFuture;
-use isahc::config::RedirectPolicy;
 use util::maybe;
 
 pub use isahc::config::Configurable;
@@ -36,18 +35,29 @@ impl HttpClient for IsahcHttpClient {
         None
     }
 
-    fn send_with_redirect_policy(
+    fn send(
         &self,
         req: http_client::http::Request<http_client::AsyncBody>,
-        follow_redirects: bool,
     ) -> 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);
@@ -64,10 +74,12 @@ impl HttpClient for IsahcHttpClient {
             };
 
             builder
-                .redirect_policy(if follow_redirects {
-                    RedirectPolicy::Follow
-                } else {
-                    RedirectPolicy::None
+                .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()

crates/language_model/Cargo.toml 🔗

@@ -32,7 +32,6 @@ futures.workspace = true
 google_ai = { workspace = true, features = ["schemars"] }
 gpui.workspace = true
 http_client.workspace = true
-isahc.workspace = true
 inline_completion_button.workspace = true
 log.workspace = true
 menu.workspace = true

crates/language_model/src/provider/cloud.rs 🔗

@@ -18,8 +18,7 @@ use gpui::{
     AnyElement, AnyView, AppContext, AsyncAppContext, FontWeight, Model, ModelContext,
     Subscription, Task,
 };
-use http_client::{AsyncBody, HttpClient, Method, Response};
-use isahc::config::Configurable;
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response};
 use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use serde_json::value::RawValue;
@@ -396,7 +395,7 @@ impl CloudLanguageModel {
         let response = loop {
             let mut request_builder = http_client::Request::builder();
             if let Some(low_speed_timeout) = low_speed_timeout {
-                request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+                request_builder = request_builder.read_timeout(low_speed_timeout);
             };
             let request = request_builder
                 .method(Method::POST)

crates/open_ai/Cargo.toml 🔗

@@ -19,7 +19,6 @@ schemars = ["dep:schemars"]
 anyhow.workspace = true
 futures.workspace = true
 http_client.workspace = true
-isahc.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true

crates/open_ai/src/open_ai.rs 🔗

@@ -6,8 +6,7 @@ use futures::{
     stream::{self, BoxStream},
     AsyncBufReadExt, AsyncReadExt, Stream, StreamExt,
 };
-use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
-use isahc::config::Configurable;
+use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use std::{
@@ -318,7 +317,7 @@ pub async fn complete(
         .header("Content-Type", "application/json")
         .header("Authorization", format!("Bearer {}", api_key));
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     };
 
     let mut request_body = request;
@@ -413,7 +412,7 @@ pub async fn stream_completion(
         .header("Authorization", format!("Bearer {}", api_key));
 
     if let Some(low_speed_timeout) = low_speed_timeout {
-        request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
+        request_builder = request_builder.read_timeout(low_speed_timeout);
     };
 
     let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;

crates/zed/Cargo.toml 🔗

@@ -57,7 +57,6 @@ http_client.workspace = true
 image_viewer.workspace = true
 inline_completion_button.workspace = true
 install_cli.workspace = true
-isahc.workspace = true
 isahc_http_client.workspace = true
 journal.workspace = true
 language.workspace = true

crates/zed/src/reliability.rs 🔗

@@ -4,8 +4,7 @@ use chrono::Utc;
 use client::telemetry;
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{AppContext, SemanticVersion};
-use http_client::Method;
-use isahc::config::Configurable;
+use http_client::{HttpRequestExt, Method};
 
 use http_client::{self, HttpClient, HttpClientWithUrl};
 use paths::{crashes_dir, crashes_retired_dir};
@@ -491,7 +490,7 @@ async fn upload_previous_crashes(
                 .context("error reading crash file")?;
 
             let mut request = http_client::Request::post(&crash_report_url.to_string())
-                .redirect_policy(isahc::config::RedirectPolicy::Follow)
+                .follow_redirects(http_client::RedirectPolicy::FollowAll)
                 .header("Content-Type", "text/plain");
 
             if let Some((panicked_on, payload)) = most_recent_panic.as_ref() {