zed_extension_api: Add `fetch` (#13716)

Marshall Bowers created

This PR adds a new `fetch` function to the `zed_extension_api` to allow
fetching a URL through the Wasm host.

Currently we only support GET requests and return the response body as a
string.

Release Notes:

- N/A

Change summary

crates/extension/src/wasm_host/wit/since_v0_0_7.rs    | 39 ++++++++++++
crates/extension_api/src/extension_api.rs             |  1 
crates/extension_api/wit/since_v0.0.7/extension.wit   |  1 
crates/extension_api/wit/since_v0.0.7/http-client.wit | 16 +++++
4 files changed, 56 insertions(+), 1 deletion(-)

Detailed changes

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

@@ -1,10 +1,12 @@
 use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
 use ::settings::Settings;
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
+use futures::AsyncReadExt;
 use futures::{io::BufReader, FutureExt as _};
+use http::AsyncBody;
 use language::{
     language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
 };
@@ -101,6 +103,41 @@ impl HostWorktree for WasmState {
 #[async_trait]
 impl common::Host for WasmState {}
 
+#[async_trait]
+impl http_client::Host for WasmState {
+    async fn fetch(
+        &mut self,
+        req: http_client::HttpRequest,
+    ) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
+        maybe!(async {
+            let url = &req.url;
+
+            let mut response = self
+                .host
+                .http_client
+                .get(url, AsyncBody::default(), true)
+                .await?;
+
+            if response.status().is_client_error() || response.status().is_server_error() {
+                bail!("failed to fetch '{url}': status code {}", response.status())
+            }
+
+            let mut body = Vec::new();
+            response
+                .body_mut()
+                .read_to_end(&mut body)
+                .await
+                .with_context(|| format!("failed to read response body from '{url}'"))?;
+
+            Ok(http_client::HttpResponse {
+                body: String::from_utf8(body)?,
+            })
+        })
+        .await
+        .to_wasmtime_result()
+    }
+}
+
 #[async_trait]
 impl nodejs::Host for WasmState {
     async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {

crates/extension_api/src/extension_api.rs 🔗

@@ -19,6 +19,7 @@ pub use wit::{
         github_release_by_tag_name, latest_github_release, GithubRelease, GithubReleaseAsset,
         GithubReleaseOptions,
     },
+    zed::extension::http_client::{fetch, HttpRequest, HttpResponse},
     zed::extension::nodejs::{
         node_binary_path, npm_install_package, npm_package_installed_version,
         npm_package_latest_version,

crates/extension_api/wit/since_v0.0.7/http-client.wit 🔗

@@ -0,0 +1,16 @@
+interface http-client {
+    /// An HTTP request.
+    record http-request {
+        /// The URL to which the request should be made.
+        url: string,
+    }
+
+    /// An HTTP response.
+    record http-response {
+        /// The response body.
+        body: string,
+    }
+
+    /// Performs an HTTP request and returns the response.
+    fetch: func(req: http-request) -> result<http-response, string>;
+}