client: Send `User-Agent` header on WebSocket connection requests (#35280)

Marshall Bowers created

This PR makes it so we send the `User-Agent` header on the WebSocket
connection requests when connecting to Collab.

We use the user agent set on the parent HTTP client.

Release Notes:

- N/A

Change summary

crates/client/src/client.rs                 |  8 ++++-
crates/gpui/src/app.rs                      |  4 +++
crates/http_client/src/http_client.rs       | 29 +++++++++++++++++++++++
crates/reqwest_client/src/reqwest_client.rs | 13 ++++++++-
4 files changed, 50 insertions(+), 4 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -21,7 +21,7 @@ use futures::{
     channel::oneshot, future::BoxFuture,
 };
 use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
-use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
+use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
 use parking_lot::RwLock;
 use postage::watch;
 use proxy::connect_proxy_stream;
@@ -1158,6 +1158,7 @@ impl Client {
 
         let http = self.http.clone();
         let proxy = http.proxy().cloned();
+        let user_agent = http.user_agent().cloned();
         let credentials = credentials.clone();
         let rpc_url = self.rpc_url(http, release_channel);
         let system_id = self.telemetry.system_id();
@@ -1209,7 +1210,7 @@ impl Client {
             // We then modify the request to add our desired headers.
             let request_headers = request.headers_mut();
             request_headers.insert(
-                "Authorization",
+                http::header::AUTHORIZATION,
                 HeaderValue::from_str(&credentials.authorization_header())?,
             );
             request_headers.insert(
@@ -1221,6 +1222,9 @@ impl Client {
                 "x-zed-release-channel",
                 HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
             );
+            if let Some(user_agent) = user_agent {
+                request_headers.insert(http::header::USER_AGENT, user_agent);
+            }
             if let Some(system_id) = system_id {
                 request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
             }

crates/gpui/src/app.rs 🔗

@@ -2023,6 +2023,10 @@ impl HttpClient for NullHttpClient {
         .boxed()
     }
 
+    fn user_agent(&self) -> Option<&http_client::http::HeaderValue> {
+        None
+    }
+
     fn proxy(&self) -> Option<&Url> {
         None
     }

crates/http_client/src/http_client.rs 🔗

@@ -4,6 +4,7 @@ pub mod github;
 pub use anyhow::{Result, anyhow};
 pub use async_body::{AsyncBody, Inner};
 use derive_more::Deref;
+use http::HeaderValue;
 pub use http::{self, Method, Request, Response, StatusCode, Uri};
 
 use futures::future::BoxFuture;
@@ -39,6 +40,8 @@ impl HttpRequestExt for http::request::Builder {
 pub trait HttpClient: 'static + Send + Sync {
     fn type_name(&self) -> &'static str;
 
+    fn user_agent(&self) -> Option<&HeaderValue>;
+
     fn send(
         &self,
         req: http::Request<AsyncBody>,
@@ -118,6 +121,10 @@ impl HttpClient for HttpClientWithProxy {
         self.client.send(req)
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        self.client.user_agent()
+    }
+
     fn proxy(&self) -> Option<&Url> {
         self.proxy.as_ref()
     }
@@ -135,6 +142,10 @@ impl HttpClient for Arc<HttpClientWithProxy> {
         self.client.send(req)
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        self.client.user_agent()
+    }
+
     fn proxy(&self) -> Option<&Url> {
         self.proxy.as_ref()
     }
@@ -250,6 +261,10 @@ impl HttpClient for Arc<HttpClientWithUrl> {
         self.client.send(req)
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        self.client.user_agent()
+    }
+
     fn proxy(&self) -> Option<&Url> {
         self.client.proxy.as_ref()
     }
@@ -267,6 +282,10 @@ impl HttpClient for HttpClientWithUrl {
         self.client.send(req)
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        self.client.user_agent()
+    }
+
     fn proxy(&self) -> Option<&Url> {
         self.client.proxy.as_ref()
     }
@@ -314,6 +333,10 @@ impl HttpClient for BlockedHttpClient {
         })
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        None
+    }
+
     fn proxy(&self) -> Option<&Url> {
         None
     }
@@ -334,6 +357,7 @@ type FakeHttpHandler = Box<
 #[cfg(feature = "test-support")]
 pub struct FakeHttpClient {
     handler: FakeHttpHandler,
+    user_agent: HeaderValue,
 }
 
 #[cfg(feature = "test-support")]
@@ -348,6 +372,7 @@ impl FakeHttpClient {
             client: HttpClientWithProxy {
                 client: Arc::new(Self {
                     handler: Box::new(move |req| Box::pin(handler(req))),
+                    user_agent: HeaderValue::from_static(type_name::<Self>()),
                 }),
                 proxy: None,
             },
@@ -390,6 +415,10 @@ impl HttpClient for FakeHttpClient {
         future
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        Some(&self.user_agent)
+    }
+
     fn proxy(&self) -> Option<&Url> {
         None
     }

crates/reqwest_client/src/reqwest_client.rs 🔗

@@ -20,6 +20,7 @@ static REDACT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"key=[^&]+")
 pub struct ReqwestClient {
     client: reqwest::Client,
     proxy: Option<Url>,
+    user_agent: Option<HeaderValue>,
     handle: tokio::runtime::Handle,
 }
 
@@ -44,9 +45,11 @@ impl ReqwestClient {
         Ok(client.into())
     }
 
-    pub fn proxy_and_user_agent(proxy: Option<Url>, agent: &str) -> anyhow::Result<Self> {
+    pub fn proxy_and_user_agent(proxy: Option<Url>, user_agent: &str) -> anyhow::Result<Self> {
+        let user_agent = HeaderValue::from_str(user_agent)?;
+
         let mut map = HeaderMap::new();
-        map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?);
+        map.insert(http::header::USER_AGENT, user_agent.clone());
         let mut client = Self::builder().default_headers(map);
         let client_has_proxy;
 
@@ -73,6 +76,7 @@ impl ReqwestClient {
             .build()?;
         let mut client: ReqwestClient = client.into();
         client.proxy = client_has_proxy.then_some(proxy).flatten();
+        client.user_agent = Some(user_agent);
         Ok(client)
     }
 }
@@ -96,6 +100,7 @@ impl From<reqwest::Client> for ReqwestClient {
             client,
             handle,
             proxy: None,
+            user_agent: None,
         }
     }
 }
@@ -216,6 +221,10 @@ impl http_client::HttpClient for ReqwestClient {
         type_name::<Self>()
     }
 
+    fn user_agent(&self) -> Option<&HeaderValue> {
+        self.user_agent.as_ref()
+    }
+
     fn send(
         &self,
         req: http::Request<http_client::AsyncBody>,