Extract `http` from `util` (#11680)

Conrad Irwin created

This avoids the CLI linking libssl etc...

Release Notes:

- N/A

Change summary

Cargo.lock                                             |  49 ++
Cargo.toml                                             |   2 
crates/anthropic/Cargo.toml                            |   2 
crates/anthropic/src/anthropic.rs                      |   4 
crates/assistant/Cargo.toml                            |   1 
crates/assistant/src/completion_provider/open_ai.rs    |   3 
crates/assistant2/Cargo.toml                           |   1 
crates/auto_update/Cargo.toml                          |   1 
crates/auto_update/src/auto_update.rs                  |   6 
crates/call/Cargo.toml                                 |   1 
crates/channel/Cargo.toml                              |   1 
crates/channel/src/channel_store_tests.rs              |   2 
crates/client/Cargo.toml                               |   2 
crates/client/src/client.rs                            |   6 
crates/client/src/http.rs                              |   1 
crates/client/src/telemetry.rs                         |   4 
crates/collab/Cargo.toml                               |   1 
crates/collab/src/rpc.rs                               |   2 
crates/collab/src/tests/test_server.rs                 |   2 
crates/collab_ui/Cargo.toml                            |   2 
crates/collab_ui/src/chat_panel/message_editor.rs      |   3 
crates/copilot/Cargo.toml                              |   2 
crates/copilot/src/copilot.rs                          |   8 
crates/editor/Cargo.toml                               |   2 
crates/editor/src/git/blame.rs                         |   2 
crates/extension/Cargo.toml                            |   1 
crates/extension/src/extension_builder.rs              |   2 
crates/extension/src/extension_store.rs                |   8 
crates/extension/src/extension_store_test.rs           |   6 
crates/extension/src/wasm_host.rs                      |   2 
crates/extension/src/wasm_host/wit/since_v0_0_6.rs     |   2 
crates/feedback/Cargo.toml                             |   1 
crates/feedback/src/feedback_modal.rs                  |   3 
crates/git/Cargo.toml                                  |   1 
crates/git/src/hosting_provider.rs                     |   2 
crates/git_hosting_providers/Cargo.toml                |   2 
crates/git_hosting_providers/src/providers/codeberg.rs |   2 
crates/git_hosting_providers/src/providers/github.rs   |   2 
crates/google_ai/Cargo.toml                            |   2 
crates/google_ai/src/google_ai.rs                      |   2 
crates/gpui/Cargo.toml                                 |   4 
crates/gpui/src/app.rs                                 |   6 
crates/gpui/src/app/test_context.rs                    |   2 
crates/gpui/src/elements/img.rs                        |   3 
crates/http/Cargo.toml                                 |  26 +
crates/http/src/github.rs                              |   2 
crates/http/src/http.rs                                | 242 ++++++++++++
crates/language/Cargo.toml                             |   2 
crates/language/src/language.rs                        |   2 
crates/languages/Cargo.toml                            |   1 
crates/languages/src/c.rs                              |   7 
crates/languages/src/go.rs                             |   3 
crates/languages/src/rust.rs                           |   7 
crates/languages/src/typescript.rs                     |   7 
crates/node_runtime/Cargo.toml                         |   1 
crates/node_runtime/src/node_runtime.rs                |   2 
crates/open_ai/Cargo.toml                              |   2 
crates/open_ai/src/open_ai.rs                          |   2 
crates/project/Cargo.toml                              |   1 
crates/project/src/project.rs                          |   9 
crates/semantic_index/Cargo.toml                       |   2 
crates/semantic_index/examples/index.rs                |   2 
crates/semantic_index/src/embedding/ollama.rs          |   2 
crates/semantic_index/src/embedding/open_ai.rs         |   2 
crates/supermaven/Cargo.toml                           |   1 
crates/supermaven_api/Cargo.toml                       |   1 
crates/supermaven_api/src/supermaven_api.rs            |   2 
crates/text/Cargo.toml                                 |   1 
crates/util/Cargo.toml                                 |   2 
crates/util/src/http.rs                                | 218 ----------
crates/util/src/util.rs                                |  24 -
crates/workspace/Cargo.toml                            |   3 
crates/workspace/src/workspace.rs                      |   2 
crates/worktree/Cargo.toml                             |   2 
crates/worktree/src/worktree_tests.rs                  |   3 
crates/zed/Cargo.toml                                  |   1 
crates/zed/src/reliability.rs                          |   8 
77 files changed, 419 insertions(+), 336 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -224,10 +224,10 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "futures 0.3.28",
+ "http 0.1.0",
  "serde",
  "serde_json",
  "tokio",
- "util",
 ]
 
 [[package]]
@@ -344,6 +344,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "indoc",
  "language",
  "log",
@@ -388,6 +389,7 @@ dependencies = [
  "futures 0.3.28",
  "fuzzy",
  "gpui",
+ "http 0.1.0",
  "language",
  "languages",
  "log",
@@ -889,6 +891,7 @@ dependencies = [
  "db",
  "editor",
  "gpui",
+ "http 0.1.0",
  "isahc",
  "log",
  "markdown_preview",
@@ -1756,6 +1759,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "language",
  "live_kit_client",
  "log",
@@ -1958,6 +1962,7 @@ dependencies = [
  "collections",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "language",
  "log",
  "rand 0.8.5",
@@ -2188,6 +2193,7 @@ dependencies = [
  "feature_flags",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "lazy_static",
  "log",
  "once_cell",
@@ -2318,6 +2324,7 @@ dependencies = [
  "gpui",
  "headless",
  "hex",
+ "http 0.1.0",
  "indoc",
  "language",
  "live_kit_client",
@@ -2387,6 +2394,7 @@ dependencies = [
  "futures 0.3.28",
  "fuzzy",
  "gpui",
+ "http 0.1.0",
  "language",
  "lazy_static",
  "menu",
@@ -2556,6 +2564,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "indoc",
  "language",
  "lsp",
@@ -3414,6 +3423,7 @@ dependencies = [
  "fuzzy",
  "git",
  "gpui",
+ "http 0.1.0",
  "indoc",
  "itertools 0.11.0",
  "language",
@@ -3717,6 +3727,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "isahc",
  "language",
  "log",
@@ -3864,6 +3875,7 @@ dependencies = [
  "editor",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "human_bytes",
  "isahc",
  "language",
@@ -4472,6 +4484,7 @@ dependencies = [
  "derive_more",
  "git2",
  "gpui",
+ "http 0.1.0",
  "lazy_static",
  "log",
  "parking_lot",
@@ -4511,6 +4524,7 @@ dependencies = [
  "futures 0.3.28",
  "git",
  "gpui",
+ "http 0.1.0",
  "isahc",
  "pretty_assertions",
  "regex",
@@ -4518,7 +4532,6 @@ dependencies = [
  "serde_json",
  "unindent",
  "url",
- "util",
 ]
 
 [[package]]
@@ -4596,9 +4609,9 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "futures 0.3.28",
+ "http 0.1.0",
  "serde",
  "serde_json",
- "util",
 ]
 
 [[package]]
@@ -4666,6 +4679,7 @@ dependencies = [
  "foreign-types 0.5.0",
  "futures 0.3.28",
  "gpui_macros",
+ "http 0.1.0",
  "image",
  "itertools 0.11.0",
  "lazy_static",
@@ -4984,6 +4998,20 @@ version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
 
+[[package]]
+name = "http"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures 0.3.28",
+ "futures-lite 1.13.0",
+ "isahc",
+ "log",
+ "serde",
+ "serde_json",
+ "url",
+]
+
 [[package]]
 name = "http"
 version = "0.2.9"
@@ -5601,6 +5629,7 @@ dependencies = [
  "git",
  "globset",
  "gpui",
+ "http 0.1.0",
  "indoc",
  "itertools 0.11.0",
  "lazy_static",
@@ -5691,6 +5720,7 @@ dependencies = [
  "feature_flags",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "language",
  "lazy_static",
  "log",
@@ -6435,6 +6465,7 @@ dependencies = [
  "async-trait",
  "async_zip",
  "futures 0.3.28",
+ "http 0.1.0",
  "log",
  "semver",
  "serde",
@@ -6829,11 +6860,11 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "futures 0.3.28",
+ "http 0.1.0",
  "isahc",
  "schemars",
  "serde",
  "serde_json",
- "util",
 ]
 
 [[package]]
@@ -7570,6 +7601,7 @@ dependencies = [
  "git2",
  "globset",
  "gpui",
+ "http 0.1.0",
  "itertools 0.11.0",
  "language",
  "log",
@@ -8841,6 +8873,7 @@ dependencies = [
  "futures-batch",
  "gpui",
  "heed",
+ "http 0.1.0",
  "language",
  "languages",
  "log",
@@ -9756,6 +9789,7 @@ dependencies = [
  "env_logger",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "language",
  "log",
  "postage",
@@ -9776,6 +9810,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "futures 0.3.28",
+ "http 0.1.0",
  "serde",
  "serde_json",
  "smol",
@@ -10157,6 +10192,7 @@ dependencies = [
  "ctor",
  "env_logger",
  "gpui",
+ "http 0.1.0",
  "lazy_static",
  "log",
  "parking_lot",
@@ -11205,7 +11241,6 @@ dependencies = [
  "futures-lite 1.13.0",
  "git2",
  "globset",
- "isahc",
  "lazy_static",
  "log",
  "rand 0.8.5",
@@ -11217,7 +11252,6 @@ dependencies = [
  "tempfile",
  "tendril",
  "unicase",
- "url",
 ]
 
 [[package]]
@@ -12619,6 +12653,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
+ "http 0.1.0",
  "itertools 0.11.0",
  "language",
  "lazy_static",
@@ -12654,6 +12689,7 @@ dependencies = [
  "git",
  "git2",
  "gpui",
+ "http 0.1.0",
  "ignore",
  "itertools 0.11.0",
  "language",
@@ -12903,6 +12939,7 @@ dependencies = [
  "go_to_line",
  "gpui",
  "headless",
+ "http 0.1.0",
  "image_viewer",
  "inline_completion_button",
  "install_cli",

Cargo.toml 🔗

@@ -41,6 +41,7 @@ members = [
     "crates/gpui",
     "crates/gpui_macros",
     "crates/headless",
+    "crates/http",
     "crates/image_viewer",
     "crates/inline_completion_button",
     "crates/install_cli",
@@ -183,6 +184,7 @@ google_ai = { path = "crates/google_ai" }
 gpui = { path = "crates/gpui" }
 gpui_macros = { path = "crates/gpui_macros" }
 headless = { path = "crates/headless" }
+http = { path = "crates/http" }
 install_cli = { path = "crates/install_cli" }
 image_viewer = { path = "crates/image_viewer" }
 inline_completion_button = { path = "crates/inline_completion_button" }

crates/anthropic/Cargo.toml 🔗

@@ -14,9 +14,9 @@ path = "src/anthropic.rs"
 [dependencies]
 anyhow.workspace = true
 futures.workspace = true
+http.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-util.workspace = true
 
 [dev-dependencies]
 tokio.workspace = true

crates/anthropic/src/anthropic.rs 🔗

@@ -1,8 +1,8 @@
 use anyhow::{anyhow, Result};
 use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
+use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
 use serde::{Deserialize, Serialize};
 use std::{convert::TryFrom, sync::Arc};
-use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
 pub enum Model {
@@ -196,7 +196,7 @@ pub async fn stream_completion(
 // #[cfg(test)]
 // mod tests {
 //     use super::*;
-//     use util::http::IsahcHttpClient;
+//     use http::IsahcHttpClient;
 
 //     #[tokio::test]
 //     async fn stream_completion_success() {

crates/assistant/Cargo.toml 🔗

@@ -20,6 +20,7 @@ file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
+http.workspace = true
 indoc.workspace = true
 language.workspace = true
 log.workspace = true

crates/assistant/src/completion_provider/open_ai.rs 🔗

@@ -5,13 +5,14 @@ use anyhow::{anyhow, Result};
 use editor::{Editor, EditorElement, EditorStyle};
 use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
 use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
+use http::HttpClient;
 use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
 use settings::Settings;
 use std::time::Duration;
 use std::{env, sync::Arc};
 use theme::ThemeSettings;
 use ui::prelude::*;
-use util::{http::HttpClient, ResultExt};
+use util::ResultExt;
 
 pub struct OpenAiCompletionProvider {
     api_key: Option<String>,

crates/assistant2/Cargo.toml 🔗

@@ -62,4 +62,5 @@ release_channel.workspace = true
 settings = { workspace = true, features = ["test-support"] }
 theme = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }

crates/auto_update/Cargo.toml 🔗

@@ -18,6 +18,7 @@ client.workspace = true
 db.workspace = true
 editor.workspace = true
 gpui.workspace = true
+http.workspace = true
 isahc.workspace = true
 log.workspace = true
 markdown_preview.workspace = true

crates/auto_update/src/auto_update.rs 🔗

@@ -20,6 +20,7 @@ use smol::{fs, io::AsyncReadExt};
 use settings::{Settings, SettingsSources, SettingsStore};
 use smol::{fs::File, process::Command};
 
+use http::{HttpClient, HttpClientWithUrl};
 use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
 use std::{
     env::consts::{ARCH, OS},
@@ -29,10 +30,7 @@ use std::{
     time::Duration,
 };
 use update_notification::UpdateNotification;
-use util::{
-    http::{HttpClient, HttpClientWithUrl},
-    ResultExt,
-};
+use util::ResultExt;
 use workspace::notifications::NotificationId;
 use workspace::Workspace;
 

crates/call/Cargo.toml 🔗

@@ -50,3 +50,4 @@ language = { workspace = true, features = ["test-support"] }
 live_kit_client = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/channel/Cargo.toml 🔗

@@ -40,3 +40,4 @@ rpc = { workspace = true, features = ["test-support"] }
 client = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/channel/src/channel_store_tests.rs 🔗

@@ -4,9 +4,9 @@ use super::*;
 use client::{test::FakeServer, Client, UserStore};
 use clock::FakeSystemClock;
 use gpui::{AppContext, Context, Model, TestAppContext};
+use http::FakeHttpClient;
 use rpc::proto::{self};
 use settings::SettingsStore;
-use util::http::FakeHttpClient;
 
 #[gpui::test]
 fn test_update_channels(cx: &mut AppContext) {

crates/client/Cargo.toml 🔗

@@ -25,6 +25,7 @@ collections.workspace = true
 feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
+http.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 once_cell.workspace = true
@@ -56,3 +57,4 @@ gpui = { workspace = true, features = ["test-support"] }
 rpc = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/client/src/client.rs 🔗

@@ -20,6 +20,7 @@ use gpui::{
     actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, BorrowAppContext, Global, Model,
     Task, WeakModel,
 };
+use http::{HttpClient, HttpClientWithUrl};
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
 use postage::watch;
@@ -47,7 +48,6 @@ use std::{
 use telemetry::Telemetry;
 use thiserror::Error;
 use url::Url;
-use util::http::{HttpClient, HttpClientWithUrl};
 use util::{ResultExt, TryFutureExt};
 
 pub use rpc::*;
@@ -204,7 +204,7 @@ pub enum EstablishConnectionError {
     #[error("{0}")]
     Other(#[from] anyhow::Error),
     #[error("{0}")]
-    Http(#[from] util::http::Error),
+    Http(#[from] http::Error),
     #[error("{0}")]
     Io(#[from] std::io::Error),
     #[error("{0}")]
@@ -1679,10 +1679,10 @@ mod tests {
 
     use clock::FakeSystemClock;
     use gpui::{BackgroundExecutor, Context, TestAppContext};
+    use http::FakeHttpClient;
     use parking_lot::Mutex;
     use settings::SettingsStore;
     use std::future;
-    use util::http::FakeHttpClient;
 
     #[gpui::test(iterations = 10)]
     async fn test_reconnection(cx: &mut TestAppContext) {

crates/client/src/telemetry.rs 🔗

@@ -5,6 +5,7 @@ use chrono::{DateTime, Utc};
 use clock::SystemClock;
 use futures::Future;
 use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
+use http::{self, HttpClient, HttpClientWithUrl, Method};
 use once_cell::sync::Lazy;
 use parking_lot::Mutex;
 use release_channel::ReleaseChannel;
@@ -19,7 +20,6 @@ use telemetry_events::{
     SettingEvent,
 };
 use tempfile::NamedTempFile;
-use util::http::{self, HttpClient, HttpClientWithUrl, Method};
 #[cfg(not(debug_assertions))]
 use util::ResultExt;
 use util::TryFutureExt;
@@ -497,7 +497,7 @@ mod tests {
     use chrono::TimeZone;
     use clock::FakeSystemClock;
     use gpui::TestAppContext;
-    use util::http::FakeHttpClient;
+    use http::FakeHttpClient;
 
     #[gpui::test]
     fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {

crates/collab/Cargo.toml 🔗

@@ -35,6 +35,7 @@ envy = "0.4.2"
 futures.workspace = true
 google_ai.workspace = true
 hex.workspace = true
+http.workspace = true
 live_kit_server.workspace = true
 log.workspace = true
 nanoid.workspace = true

crates/collab/src/rpc.rs 🔗

@@ -42,6 +42,7 @@ use futures::{
     stream::FuturesUnordered,
     FutureExt, SinkExt, StreamExt, TryStreamExt,
 };
+use http::IsahcHttpClient;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
@@ -73,7 +74,6 @@ use tracing::{
     field::{self},
     info_span, instrument, Instrument,
 };
-use util::http::IsahcHttpClient;
 
 use self::connection_pool::VersionedMessage;
 

crates/collab/src/tests/test_server.rs 🔗

@@ -19,6 +19,7 @@ use fs::FakeFs;
 use futures::{channel::oneshot, StreamExt as _};
 use git::GitHostingProviderRegistry;
 use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
+use http::FakeHttpClient;
 use language::LanguageRegistry;
 use node_runtime::FakeNodeRuntime;
 use notifications::NotificationStore;
@@ -41,7 +42,6 @@ use std::{
         Arc,
     },
 };
-use util::http::FakeHttpClient;
 use workspace::{Workspace, WorkspaceId, WorkspaceStore};
 
 pub struct TestServer {

crates/collab_ui/Cargo.toml 🔗

@@ -25,6 +25,7 @@ test-support = [
     "settings/test-support",
     "util/test-support",
     "workspace/test-support",
+    "http/test-support",
 ]
 
 [dependencies]
@@ -84,4 +85,5 @@ rpc = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 tree-sitter-markdown.workspace = true
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -557,11 +557,12 @@ mod tests {
     use client::{Client, User, UserStore};
     use clock::FakeSystemClock;
     use gpui::TestAppContext;
+    use http::FakeHttpClient;
     use language::{Language, LanguageConfig};
     use project::Project;
     use rpc::proto;
     use settings::SettingsStore;
-    use util::{http::FakeHttpClient, test::marked_text_ranges};
+    use util::test::marked_text_ranges;
 
     #[gpui::test]
     async fn test_message_editor(cx: &mut TestAppContext) {

crates/copilot/Cargo.toml 🔗

@@ -32,6 +32,7 @@ command_palette_hooks.workspace = true
 editor.workspace = true
 futures.workspace = true
 gpui.workspace = true
+http.workspace = true
 language.workspace = true
 lsp.workspace = true
 menu.workspace = true
@@ -63,3 +64,4 @@ rpc = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 theme = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/copilot/src/copilot.rs 🔗

@@ -12,6 +12,8 @@ use gpui::{
     actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
     ModelContext, Task, WeakModel,
 };
+use http::github::latest_github_release;
+use http::HttpClient;
 use language::{
     language_settings::{all_language_settings, language_settings, InlineCompletionProvider},
     point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
@@ -31,9 +33,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{
-    fs::remove_matching, github::latest_github_release, http::HttpClient, maybe, paths, ResultExt,
-};
+use util::{fs::remove_matching, maybe, paths, ResultExt};
 
 pub use copilot_completion_provider::CopilotCompletionProvider;
 pub use sign_in::CopilotCodeVerification;
@@ -393,7 +393,7 @@ impl Copilot {
             Default::default(),
             cx.to_async(),
         );
-        let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
+        let http = http::FakeHttpClient::create(|_| async { unreachable!() });
         let node_runtime = FakeNodeRuntime::new();
         let this = cx.new_model(|cx| Self {
             server_id: LanguageServerId(0),

crates/editor/Cargo.toml 🔗

@@ -39,6 +39,7 @@ futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true
 gpui.workspace = true
+http.workspace = true
 indoc.workspace = true
 itertools.workspace = true
 language.workspace = true
@@ -91,3 +92,4 @@ tree-sitter-typescript.workspace = true
 unindent.workspace = true
 util = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/editor/src/git/blame.rs 🔗

@@ -7,13 +7,13 @@ use git::{
     parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest,
 };
 use gpui::{Model, ModelContext, Subscription, Task};
+use http::HttpClient;
 use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
 use multi_buffer::MultiBufferRow;
 use project::{Item, Project};
 use smallvec::SmallVec;
 use sum_tree::SumTree;
 use url::Url;
-use util::http::HttpClient;
 
 #[derive(Clone, Debug, Default)]
 pub struct GitBlameEntry {

crates/extension/Cargo.toml 🔗

@@ -23,6 +23,7 @@ collections.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
+http.workspace = true
 isahc.workspace = true
 language.workspace = true
 log.workspace = true

crates/extension/src/extension_builder.rs 🔗

@@ -6,6 +6,7 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use futures::io::BufReader;
 use futures::AsyncReadExt;
+use http::{self, AsyncBody, HttpClient};
 use serde::Deserialize;
 use std::{
     env, fs, mem,
@@ -13,7 +14,6 @@ use std::{
     process::{Command, Stdio},
     sync::Arc,
 };
-use util::http::{self, AsyncBody, HttpClient};
 use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
 use wasmparser::Parser;
 use wit_component::ComponentEncoder;

crates/extension/src/extension_store.rs 🔗

@@ -28,6 +28,7 @@ use gpui::{
     actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
     WeakModel,
 };
+use http::{AsyncBody, HttpClient, HttpClientWithUrl};
 use language::{
     ContextProviderWithTasks, LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry,
     QUERY_FILENAME_PREFIXES,
@@ -46,12 +47,7 @@ use std::{
 };
 use theme::{ThemeRegistry, ThemeSettings};
 use url::Url;
-use util::{
-    http::{AsyncBody, HttpClient, HttpClientWithUrl},
-    maybe,
-    paths::EXTENSIONS_DIR,
-    ResultExt,
-};
+use util::{maybe, paths::EXTENSIONS_DIR, ResultExt};
 use wasm_host::{
     wit::{is_supported_wasm_api_version, wasm_api_version_range},
     WasmExtension, WasmHost,

crates/extension/src/extension_store_test.rs 🔗

@@ -10,6 +10,7 @@ use collections::BTreeMap;
 use fs::{FakeFs, Fs, RealFs};
 use futures::{io::BufReader, AsyncReadExt, StreamExt};
 use gpui::{Context, TestAppContext};
+use http::{FakeHttpClient, Response};
 use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 use node_runtime::FakeNodeRuntime;
 use parking_lot::Mutex;
@@ -22,10 +23,7 @@ use std::{
     sync::Arc,
 };
 use theme::ThemeRegistry;
-use util::{
-    http::{FakeHttpClient, Response},
-    test::temp_tree,
-};
+use util::test::temp_tree;
 
 #[cfg(test)]
 #[ctor::ctor]

crates/extension/src/wasm_host.rs 🔗

@@ -13,6 +13,7 @@ use futures::{
     Future, FutureExt, StreamExt as _,
 };
 use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
+use http::HttpClient;
 use language::LanguageRegistry;
 use node_runtime::NodeRuntime;
 use semantic_version::SemanticVersion;
@@ -20,7 +21,6 @@ use std::{
     path::{Path, PathBuf},
     sync::{Arc, OnceLock},
 };
-use util::http::HttpClient;
 use wasmtime::{
     component::{Component, ResourceTable},
     Engine, Store,

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

@@ -155,7 +155,7 @@ impl github::Host for WasmState {
         options: github::GithubReleaseOptions,
     ) -> wasmtime::Result<Result<github::GithubRelease, String>> {
         maybe!(async {
-            let release = util::github::latest_github_release(
+            let release = http::github::latest_github_release(
                 &repo,
                 options.require_assets,
                 options.pre_release,

crates/feedback/Cargo.toml 🔗

@@ -24,6 +24,7 @@ futures.workspace = true
 gpui.workspace = true
 human_bytes = "0.4.1"
 isahc.workspace = true
+http.workspace = true
 language.workspace = true
 log.workspace = true
 menu.workspace = true

crates/feedback/src/feedback_modal.rs 🔗

@@ -10,13 +10,14 @@ use gpui::{
     div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
     PromptLevel, Render, Task, View, ViewContext,
 };
+use http::HttpClient;
 use isahc::Request;
 use language::Buffer;
 use project::Project;
 use regex::Regex;
 use serde_derive::Serialize;
 use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
-use util::{http::HttpClient, ResultExt};
+use util::ResultExt;
 use workspace::notifications::NotificationId;
 use workspace::{DismissDecision, ModalView, Toast, Workspace};
 

crates/git/Cargo.toml 🔗

@@ -19,6 +19,7 @@ collections.workspace = true
 derive_more.workspace = true
 git2.workspace = true
 gpui.workspace = true
+http.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true

crates/git/src/hosting_provider.rs 🔗

@@ -5,9 +5,9 @@ use async_trait::async_trait;
 use collections::BTreeMap;
 use derive_more::{Deref, DerefMut};
 use gpui::{AppContext, Global};
+use http::HttpClient;
 use parking_lot::RwLock;
 use url::Url;
-use util::http::HttpClient;
 
 use crate::Oid;
 

crates/git_hosting_providers/Cargo.toml 🔗

@@ -17,12 +17,12 @@ async-trait.workspace = true
 futures.workspace = true
 git.workspace = true
 gpui.workspace = true
+http.workspace = true
 isahc.workspace = true
 regex.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 url.workspace = true
-util.workspace = true
 
 [dev-dependencies]
 unindent.workspace = true

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

@@ -3,11 +3,11 @@ use std::sync::Arc;
 use anyhow::{bail, Context, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
+use http::HttpClient;
 use isahc::config::Configurable;
 use isahc::{AsyncBody, Request};
 use serde::Deserialize;
 use url::Url;
-use util::http::HttpClient;
 
 use git::{
     BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote,

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

@@ -3,12 +3,12 @@ use std::sync::{Arc, OnceLock};
 use anyhow::{bail, Context, Result};
 use async_trait::async_trait;
 use futures::AsyncReadExt;
+use http::HttpClient;
 use isahc::config::Configurable;
 use isahc::{AsyncBody, Request};
 use regex::Regex;
 use serde::Deserialize;
 use url::Url;
-use util::http::HttpClient;
 
 use git::{
     BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote,

crates/google_ai/Cargo.toml 🔗

@@ -11,6 +11,6 @@ path = "src/google_ai.rs"
 [dependencies]
 anyhow.workspace = true
 futures.workspace = true
+http.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-util.workspace = true

crates/google_ai/src/google_ai.rs 🔗

@@ -2,8 +2,8 @@ use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
 use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
+use http::HttpClient;
 use serde::{Deserialize, Serialize};
-use util::http::HttpClient;
 
 pub const API_URL: &str = "https://generativelanguage.googleapis.com";
 

crates/gpui/Cargo.toml 🔗

@@ -12,7 +12,7 @@ workspace = true
 
 [features]
 default = []
-test-support = ["backtrace", "collections/test-support", "util/test-support"]
+test-support = ["backtrace", "collections/test-support", "util/test-support", "http/test-support"]
 runtime_shaders = []
 macos-blade = ["blade-graphics", "blade-macros", "bytemuck"]
 
@@ -35,6 +35,7 @@ etagere = "0.2"
 futures.workspace = true
 font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4" }
 gpui_macros.workspace = true
+http.workspace = true
 image = "0.23"
 itertools.workspace = true
 lazy_static.workspace = true
@@ -72,6 +73,7 @@ waker-fn = "1.1.0"
 backtrace = "0.3"
 collections = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }
 
 [target.'cfg(target_os = "macos")'.build-dependencies]
 bindgen = "0.65.1"

crates/gpui/src/app.rs 🔗

@@ -19,13 +19,11 @@ use time::UtcOffset;
 pub use async_context::*;
 use collections::{FxHashMap, FxHashSet, VecDeque};
 pub use entity_map::*;
+use http::{self, HttpClient};
 pub use model_context::*;
 #[cfg(any(test, feature = "test-support"))]
 pub use test_context::*;
-use util::{
-    http::{self, HttpClient},
-    ResultExt,
-};
+use util::ResultExt;
 
 use crate::{
     current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,

crates/gpui/src/app/test_context.rs 🔗

@@ -104,7 +104,7 @@ impl TestAppContext {
         let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
         let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
         let asset_source = Arc::new(());
-        let http_client = util::http::FakeHttpClient::with_404_response();
+        let http_client = http::FakeHttpClient::with_404_response();
         let text_system = Arc::new(TextSystem::new(platform.text_system()));
 
         Self {

crates/gpui/src/elements/img.rs 🔗

@@ -13,8 +13,9 @@ use image::{ImageBuffer, ImageError};
 #[cfg(target_os = "macos")]
 use media::core_video::CVImageBuffer;
 
+use http;
 use thiserror::Error;
-use util::{http, ResultExt};
+use util::ResultExt;
 
 /// A source of image content.
 #[derive(Clone, Debug)]

crates/http/Cargo.toml 🔗

@@ -0,0 +1,26 @@
+[package]
+name = "http"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "Apache-2.0"
+
+[lints]
+workspace = true
+
+[features]
+test-support = []
+
+[lib]
+path = "src/http.rs"
+doctest = true
+
+[dependencies]
+anyhow.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

crates/util/src/github.rs → crates/http/src/github.rs 🔗

@@ -1,4 +1,4 @@
-use crate::http::HttpClient;
+use crate::HttpClient;
 use anyhow::{anyhow, bail, Context, Result};
 use futures::AsyncReadExt;
 use serde::Deserialize;

crates/http/src/http.rs 🔗

@@ -0,0 +1,242 @@
+pub mod github;
+
+pub use anyhow::{anyhow, Result};
+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;
+
+fn http_proxy_from_env() -> Option<isahc::http::Uri> {
+    macro_rules! try_env {
+        ($($env:literal),+) => {
+            $(
+                if let Ok(env) = std::env::var($env) {
+                    return env.parse::<isahc::http::Uri>().ok();
+                }
+            )+
+        };
+    }
+
+    try_env!(
+        "ALL_PROXY",
+        "all_proxy",
+        "HTTPS_PROXY",
+        "https_proxy",
+        "HTTP_PROXY",
+        "http_proxy"
+    );
+    None
+}
+
+/// An [`HttpClient`] that has a base URL.
+pub struct HttpClientWithUrl {
+    base_url: Mutex<String>,
+    client: Arc<dyn HttpClient>,
+}
+
+impl HttpClientWithUrl {
+    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
+    pub fn new(base_url: impl Into<String>) -> Self {
+        Self {
+            base_url: Mutex::new(base_url.into()),
+            client: 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)
+    }
+}
+
+impl HttpClient for HttpClientWithUrl {
+    fn send(
+        &self,
+        req: Request<AsyncBody>,
+    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
+        self.client.send(req)
+    }
+}
+
+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(),
+        }
+    }
+}
+
+pub fn client() -> Arc<dyn HttpClient> {
+    Arc::new(
+        isahc::HttpClient::builder()
+            .connect_timeout(Duration::from_secs(5))
+            .low_speed_timeout(100, Duration::from_secs(5))
+            .proxy(http_proxy_from_env())
+            .build()
+            .unwrap(),
+    )
+}
+
+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 })
+    }
+}
+
+#[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: Arc::new(Self {
+                handler: Box::new(move |req| Box::pin(handler(req))),
+            }),
+        })
+    }
+
+    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) })
+    }
+}

crates/language/Cargo.toml 🔗

@@ -34,6 +34,7 @@ fuzzy.workspace = true
 git.workspace = true
 globset.workspace = true
 gpui.workspace = true
+http.workspace = true
 itertools.workspace = true
 lazy_static.workspace = true
 log.workspace = true
@@ -82,3 +83,4 @@ tree-sitter-rust.workspace = true
 tree-sitter-typescript.workspace = true
 unindent.workspace = true
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/language/src/language.rs 🔗

@@ -27,6 +27,7 @@ use collections::{HashMap, HashSet};
 use futures::Future;
 use gpui::{AppContext, AsyncAppContext, Model, Task};
 pub use highlight_map::HighlightMap;
+use http::HttpClient;
 use lazy_static::lazy_static;
 use lsp::{CodeActionKind, LanguageServerBinary};
 use parking_lot::Mutex;
@@ -62,7 +63,6 @@ pub use task_context::{
 };
 use theme::SyntaxTheme;
 use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
-use util::http::HttpClient;
 
 pub use buffer::Operation;
 pub use buffer::*;

crates/languages/Cargo.toml 🔗

@@ -17,6 +17,7 @@ collections.workspace = true
 feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
+http.workspace = true
 language.workspace = true
 lazy_static.workspace = true
 log.workspace = true

crates/languages/src/c.rs 🔗

@@ -2,17 +2,14 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 use gpui::AsyncAppContext;
+use http::github::{latest_github_release, GitHubLspBinaryVersion};
 pub use language::*;
 use lsp::LanguageServerBinary;
 use project::project_settings::{BinarySettings, ProjectSettings};
 use settings::Settings;
 use smol::fs::{self, File};
 use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
-use util::{
-    fs::remove_matching,
-    github::{latest_github_release, GitHubLspBinaryVersion},
-    maybe, ResultExt,
-};
+use util::{fs::remove_matching, maybe, ResultExt};
 
 pub struct CLspAdapter;
 

crates/languages/src/go.rs 🔗

@@ -2,6 +2,7 @@ use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 use gpui::{AsyncAppContext, Task};
+use http::github::latest_github_release;
 pub use language::*;
 use lazy_static::lazy_static;
 use lsp::LanguageServerBinary;
@@ -21,7 +22,7 @@ use std::{
         Arc,
     },
 };
-use util::{fs::remove_matching, github::latest_github_release, maybe, ResultExt};
+use util::{fs::remove_matching, maybe, ResultExt};
 
 fn server_binary_arguments() -> Vec<OsString> {
     vec!["-mode=stdio".into()]

crates/languages/src/rust.rs 🔗

@@ -3,6 +3,7 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
 use futures::{io::BufReader, StreamExt};
 use gpui::AsyncAppContext;
+use http::github::{latest_github_release, GitHubLspBinaryVersion};
 pub use language::*;
 use lazy_static::lazy_static;
 use lsp::LanguageServerBinary;
@@ -18,11 +19,7 @@ use std::{
     sync::Arc,
 };
 use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
-use util::{
-    fs::remove_matching,
-    github::{latest_github_release, GitHubLspBinaryVersion},
-    maybe, ResultExt,
-};
+use util::{fs::remove_matching, maybe, ResultExt};
 
 pub struct RustLspAdapter;
 

crates/languages/src/typescript.rs 🔗

@@ -4,6 +4,7 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use collections::HashMap;
 use gpui::AsyncAppContext;
+use http::github::{build_tarball_url, GitHubLspBinaryVersion};
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::{CodeActionKind, LanguageServerBinary};
 use node_runtime::NodeRuntime;
@@ -17,11 +18,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{
-    fs::remove_matching,
-    github::{build_tarball_url, GitHubLspBinaryVersion},
-    maybe, ResultExt,
-};
+use util::{fs::remove_matching, maybe, ResultExt};
 
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]

crates/node_runtime/Cargo.toml 🔗

@@ -22,6 +22,7 @@ async-tar.workspace = true
 async-trait.workspace = true
 async_zip.workspace = true
 futures.workspace = true
+http.workspace = true
 log.workspace = true
 semver.workspace = true
 serde.workspace = true

crates/node_runtime/src/node_runtime.rs 🔗

@@ -4,6 +4,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use futures::AsyncReadExt;
+use http::HttpClient;
 use semver::Version;
 use serde::Deserialize;
 use smol::io::BufReader;
@@ -15,7 +16,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::ResultExt;
 
 #[cfg(windows)]

crates/open_ai/Cargo.toml 🔗

@@ -15,8 +15,8 @@ schemars = ["dep:schemars"]
 [dependencies]
 anyhow.workspace = true
 futures.workspace = true
+http.workspace = true
 isahc.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
-util.workspace = true

crates/open_ai/src/open_ai.rs 🔗

@@ -1,11 +1,11 @@
 use anyhow::{anyhow, Context, Result};
 use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
+use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
 use isahc::config::Configurable;
 use serde::{Deserialize, Serialize};
 use serde_json::{Map, Value};
 use std::time::Duration;
 use std::{convert::TryFrom, future::Future};
-use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
 
 pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
 

crates/project/Cargo.toml 🔗

@@ -36,6 +36,7 @@ fuzzy.workspace = true
 git.workspace = true
 globset.workspace = true
 gpui.workspace = true
+http.workspace = true
 itertools.workspace = true
 language.workspace = true
 log.workspace = true

crates/project/src/project.rs 🔗

@@ -69,6 +69,7 @@ use rand::prelude::*;
 use search_history::SearchHistory;
 use worktree::LocalSnapshot;
 
+use http::{HttpClient, Url};
 use rpc::{ErrorCode, ErrorExt as _};
 use search::SearchQuery;
 use serde::Serialize;
@@ -99,9 +100,7 @@ use task::static_source::{StaticSource, TrackedFile};
 use terminals::Terminals;
 use text::{Anchor, BufferId, LineEnding};
 use util::{
-    debug_panic, defer,
-    http::{HttpClient, Url},
-    maybe, merge_json_value_into, parse_env_output,
+    debug_panic, defer, maybe, merge_json_value_into, parse_env_output,
     paths::{
         LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH, LOCAL_VSCODE_TASKS_RELATIVE_PATH,
     },
@@ -1003,7 +1002,7 @@ impl Project {
         let fs = Arc::new(RealFs::default());
         let languages = LanguageRegistry::test(cx.background_executor().clone());
         let clock = Arc::new(FakeSystemClock::default());
-        let http_client = util::http::FakeHttpClient::with_404_response();
+        let http_client = http::FakeHttpClient::with_404_response();
         let client = cx
             .update(|cx| client::Client::new(clock, http_client.clone(), cx))
             .unwrap();
@@ -1047,7 +1046,7 @@ impl Project {
 
         let languages = LanguageRegistry::test(cx.executor());
         let clock = Arc::new(FakeSystemClock::default());
-        let http_client = util::http::FakeHttpClient::with_404_response();
+        let http_client = http::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
         let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
         let project = cx.update(|cx| {

crates/semantic_index/Cargo.toml 🔗

@@ -29,6 +29,7 @@ gpui.workspace = true
 language.workspace = true
 log.workspace = true
 heed.workspace = true
+http.workspace = true
 open_ai.workspace = true
 parking_lot.workspace = true
 project.workspace = true
@@ -58,3 +59,4 @@ tempfile.workspace = true
 util = { workspace = true, features = ["test-support"] }
 worktree = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/semantic_index/examples/index.rs 🔗

@@ -1,6 +1,7 @@
 use client::Client;
 use futures::channel::oneshot;
 use gpui::App;
+use http::HttpClientWithUrl;
 use language::language_settings::AllLanguageSettings;
 use project::Project;
 use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex};
@@ -9,7 +10,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClientWithUrl;
 
 fn main() {
     env_logger::init();

crates/semantic_index/src/embedding/ollama.rs 🔗

@@ -1,8 +1,8 @@
 use anyhow::{Context as _, Result};
 use futures::{future::BoxFuture, AsyncReadExt, FutureExt};
+use http::HttpClient;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use util::http::HttpClient;
 
 use crate::{Embedding, EmbeddingProvider, TextToEmbed};
 

crates/semantic_index/src/embedding/open_ai.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{Embedding, EmbeddingProvider, TextToEmbed};
 use anyhow::Result;
 use futures::{future::BoxFuture, FutureExt};
+use http::HttpClient;
 pub use open_ai::OpenAiEmbeddingModel;
 use std::sync::Arc;
-use util::http::HttpClient;
 
 pub struct OpenAiEmbeddingProvider {
     client: Arc<dyn HttpClient>,

crates/supermaven/Cargo.toml 🔗

@@ -39,3 +39,4 @@ project = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
 theme = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/supermaven_api/Cargo.toml 🔗

@@ -15,6 +15,7 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 futures.workspace = true
+http.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 smol.workspace = true

crates/supermaven_api/src/supermaven_api.rs 🔗

@@ -1,12 +1,12 @@
 use anyhow::{anyhow, Context, Result};
 use futures::io::BufReader;
 use futures::{AsyncReadExt, Future};
+use http::{AsyncBody, HttpClient, Request as HttpRequest};
 use serde::{Deserialize, Serialize};
 use smol::fs::{self, File};
 use smol::stream::StreamExt;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
-use util::http::{AsyncBody, HttpClient, Request as HttpRequest};
 use util::paths::SUPERMAVEN_DIR;
 
 #[derive(Serialize)]

crates/text/Cargo.toml 🔗

@@ -37,3 +37,4 @@ env_logger.workspace = true
 gpui = { workspace = true, features = ["test-support"] }
 rand.workspace = true
 util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }

crates/util/Cargo.toml 🔗

@@ -22,7 +22,6 @@ dirs = "3.0"
 futures.workspace = true
 git2 = { workspace = true, optional = true }
 globset.workspace = true
-isahc.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 rand.workspace = true
@@ -35,7 +34,6 @@ futures-lite.workspace = true
 take-until = "0.2.0"
 tempfile = { workspace = true, optional = true }
 unicase.workspace = true
-url.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 tendril = "0.4.3"

crates/util/src/http.rs 🔗

@@ -1,219 +1 @@
-use crate::http_proxy_from_env;
-pub use anyhow::{anyhow, Result};
-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;
 
-/// An [`HttpClient`] that has a base URL.
-pub struct HttpClientWithUrl {
-    base_url: Mutex<String>,
-    client: Arc<dyn HttpClient>,
-}
-
-impl HttpClientWithUrl {
-    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
-    pub fn new(base_url: impl Into<String>) -> Self {
-        Self {
-            base_url: Mutex::new(base_url.into()),
-            client: 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)
-    }
-}
-
-impl HttpClient for HttpClientWithUrl {
-    fn send(
-        &self,
-        req: Request<AsyncBody>,
-    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
-        self.client.send(req)
-    }
-}
-
-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(),
-        }
-    }
-}
-
-pub fn client() -> Arc<dyn HttpClient> {
-    Arc::new(
-        isahc::HttpClient::builder()
-            .connect_timeout(Duration::from_secs(5))
-            .low_speed_timeout(100, Duration::from_secs(5))
-            .proxy(http_proxy_from_env())
-            .build()
-            .unwrap(),
-    )
-}
-
-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 })
-    }
-}
-
-#[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: Arc::new(Self {
-                handler: Box::new(move |req| Box::pin(handler(req))),
-            }),
-        })
-    }
-
-    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) })
-    }
-}

crates/util/src/util.rs 🔗

@@ -1,7 +1,5 @@
 pub mod arc_cow;
 pub mod fs;
-pub mod github;
-pub mod http;
 pub mod paths;
 pub mod serde;
 #[cfg(any(test, feature = "test-support"))]
@@ -43,28 +41,6 @@ pub fn truncate(s: &str, max_chars: usize) -> &str {
     }
 }
 
-pub fn http_proxy_from_env() -> Option<isahc::http::Uri> {
-    macro_rules! try_env {
-        ($($env:literal),+) => {
-            $(
-                if let Ok(env) = std::env::var($env) {
-                    return env.parse::<isahc::http::Uri>().ok();
-                }
-            )+
-        };
-    }
-
-    try_env!(
-        "ALL_PROXY",
-        "all_proxy",
-        "HTTPS_PROXY",
-        "https_proxy",
-        "HTTP_PROXY",
-        "http_proxy"
-    );
-    None
-}
-
 /// Removes characters from the end of the string if its length is greater than `max_chars` and
 /// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars.
 pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {

crates/workspace/Cargo.toml 🔗

@@ -16,6 +16,7 @@ doctest = false
 test-support = [
     "call/test-support",
     "client/test-support",
+    "http/test-support",
     "db/test-support",
     "project/test-support",
     "settings/test-support",
@@ -37,6 +38,7 @@ derive_more.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
+http.workspace = true
 itertools.workspace = true
 language.workspace = true
 lazy_static.workspace = true
@@ -67,3 +69,4 @@ fs = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 project = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
+http =  { workspace = true, features = ["test-support"] }

crates/workspace/src/workspace.rs 🔗

@@ -463,7 +463,7 @@ impl AppState {
         let fs = fs::FakeFs::new(cx.background_executor().clone());
         let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let clock = Arc::new(clock::FakeSystemClock::default());
-        let http_client = util::http::FakeHttpClient::with_404_response();
+        let http_client = http::FakeHttpClient::with_404_response();
         let client = Client::new(clock, http_client.clone(), cx);
         let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));

crates/worktree/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
     "settings/test-support",
     "text/test-support",
     "gpui/test-support",
+    "http/test-support",
 ]
 
 [dependencies]
@@ -53,6 +54,7 @@ clock = {workspace = true, features = ["test-support"]}
 collections = { workspace = true, features = ["test-support"] }
 git2.workspace = true
 gpui = {workspace = true, features = ["test-support"]}
+http.workspace = true
 rand.workspace = true
 settings = {workspace = true, features = ["test-support"]}
 pretty_assertions.workspace = true

crates/worktree/src/worktree_tests.rs 🔗

@@ -8,6 +8,7 @@ use clock::FakeSystemClock;
 use fs::{FakeFs, Fs, RealFs, RemoveOptions};
 use git::{repository::GitFileStatus, GITIGNORE};
 use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
+use http::FakeHttpClient;
 use parking_lot::Mutex;
 use postage::stream::Stream;
 use pretty_assertions::assert_eq;
@@ -21,7 +22,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
+use util::{test::temp_tree, ResultExt};
 
 #[gpui::test]
 async fn test_traversal(cx: &mut TestAppContext) {

crates/zed/Cargo.toml 🔗

@@ -50,6 +50,7 @@ git_hosting_providers.workspace = true
 go_to_line.workspace = true
 gpui.workspace = true
 headless.workspace = true
+http.workspace = true
 image_viewer.workspace = true
 inline_completion_button.workspace = true
 install_cli.workspace = true

crates/zed/src/reliability.rs 🔗

@@ -5,6 +5,7 @@ use db::kvp::KEY_VALUE_STORE;
 use gpui::{App, AppContext, SemanticVersion};
 use isahc::config::Configurable;
 
+use http::{self, HttpClient, HttpClientWithUrl};
 use paths::{CRASHES_DIR, CRASHES_RETIRED_DIR};
 use release_channel::ReleaseChannel;
 use release_channel::RELEASE_CHANNEL;
@@ -17,10 +18,7 @@ use std::{
     sync::{atomic::Ordering, Arc},
 };
 use std::{io::Write, panic, sync::atomic::AtomicU32, thread};
-use util::{
-    http::{self, HttpClient, HttpClientWithUrl},
-    paths, ResultExt,
-};
+use util::{paths, ResultExt};
 
 use crate::stdout_is_a_pty;
 
@@ -198,13 +196,13 @@ pub fn monitor_main_thread_hangs(
 
     use parking_lot::Mutex;
 
+    use http::Method;
     use std::{
         ffi::c_int,
         sync::{mpsc, OnceLock},
         time::Duration,
     };
     use telemetry_events::{BacktraceFrame, HangReport};
-    use util::http::Method;
 
     use nix::sys::pthread;