http_client: Support `GITHUB_TOKEN` env to auth GitHub requests (#42623)

Binlogo created

Closes #33903

Release Notes:

- Ensured Zed reuses `GITHUB_TOKEN` env variable when querying GitHub

---

Before fixing:

-  The `crates-lsp` extension request captured:
```
curl 'https://api.github.com/repos/MathiasPius/crates-lsp/releases' \
-H 'accept: */*' \
-H 'user-agent: Zed/0.212.3 (macos; aarch64)' \
-H 'host: api.github.com' \
```

-  `crates-lsp` extension error: 
```
Language server crates-lsp:

from extension "Crates LSP" version 0.2.0: status error 403, response: "{\"message\":\"API rate limit exceeded for x.x.x.x. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)\",\"documentation_url\":\"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting\"}\n"
```

After fixing:

```
export GITHUB_TOKEN=$(gh auth token)
cargo run
```

-  The `crates-lsp` extension request captured:
```
curl 'https://api.github.com/repos/MathiasPius/crates-lsp/releases' \
-H 'authorization: Bearer gho_Nt*****************2KXLw2' \
-H 'accept: */*' \
-H 'user-agent: Zed/0.214.0 (macos; aarch64)' \
-H 'host: api.github.com' \
```

The API rate limitation is resolved.

---

This isn't a perfect solution, but it enables users to avoid the noise.

Change summary

crates/http_client/src/github.rs | 35 +++++++++++++++++++++++----------
1 file changed, 24 insertions(+), 11 deletions(-)

Detailed changes

crates/http_client/src/github.rs 🔗

@@ -1,10 +1,13 @@
-use crate::HttpClient;
+use crate::{HttpClient, HttpRequestExt};
 use anyhow::{Context as _, Result, anyhow, bail};
 use futures::AsyncReadExt;
+use http::Request;
 use serde::Deserialize;
 use std::sync::Arc;
 use url::Url;
 
+const GITHUB_API_URL: &str = "https://api.github.com";
+
 pub struct GitHubLspBinaryVersion {
     pub name: String,
     pub url: String,
@@ -34,12 +37,17 @@ pub async fn latest_github_release(
     pre_release: bool,
     http: Arc<dyn HttpClient>,
 ) -> anyhow::Result<GithubRelease> {
+    let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");
+
+    let request = Request::get(&url)
+        .follow_redirects(crate::RedirectPolicy::FollowAll)
+        .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
+            builder.header("Authorization", format!("Bearer {}", token))
+        })
+        .body(Default::default())?;
+
     let mut response = http
-        .get(
-            format!("https://api.github.com/repos/{repo_name_with_owner}/releases").as_str(),
-            Default::default(),
-            true,
-        )
+        .send(request)
         .await
         .context("error fetching latest release")?;
 
@@ -91,12 +99,17 @@ pub async fn get_release_by_tag_name(
     tag: &str,
     http: Arc<dyn HttpClient>,
 ) -> anyhow::Result<GithubRelease> {
+    let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");
+
+    let request = Request::get(&url)
+        .follow_redirects(crate::RedirectPolicy::FollowAll)
+        .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
+            builder.header("Authorization", format!("Bearer {}", token))
+        })
+        .body(Default::default())?;
+
     let mut response = http
-        .get(
-            &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
-            Default::default(),
-            true,
-        )
+        .send(request)
         .await
         .context("error fetching latest release")?;