Redact Google Gemini API keys from error messaging and log (#24884)

Peter Tripp created

Now:
```
ERROR assistant_context_editor] error sending request for url (https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:countTokens?key=REDACTED)
```

Release Notes:

- Improved redaction of Google Gemini keys from API errors in logs

Change summary

Cargo.lock                                  |  1 +
crates/reqwest_client/Cargo.toml            |  1 +
crates/reqwest_client/src/reqwest_client.rs | 21 +++++++++++++++++++--
3 files changed, 21 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11136,6 +11136,7 @@ dependencies = [
  "gpui",
  "http_client",
  "log",
+ "regex",
  "reqwest 0.12.8",
  "serde",
  "smol",

crates/reqwest_client/Cargo.toml 🔗

@@ -28,6 +28,7 @@ serde.workspace = true
 smol.workspace = true
 log.workspace = true
 tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
+regex.workspace = true
 reqwest.workspace = true
 
 [dev-dependencies]

crates/reqwest_client/src/reqwest_client.rs 🔗

@@ -1,9 +1,11 @@
-use std::{any::type_name, mem, pin::Pin, sync::OnceLock, task::Poll, time::Duration};
+use std::sync::{LazyLock, OnceLock};
+use std::{any::type_name, borrow::Cow, mem, pin::Pin, task::Poll, time::Duration};
 
 use anyhow::anyhow;
 use bytes::{BufMut, Bytes, BytesMut};
 use futures::{AsyncRead, TryStreamExt as _};
 use http_client::{http, RedirectPolicy};
+use regex::Regex;
 use reqwest::{
     header::{HeaderMap, HeaderValue},
     redirect,
@@ -12,6 +14,7 @@ use smol::future::FutureExt;
 
 const DEFAULT_CAPACITY: usize = 4096;
 static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
+static REDACT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"key=[^&]+").unwrap());
 
 pub struct ReqwestClient {
     client: reqwest::Client,
@@ -180,6 +183,17 @@ pub fn poll_read_buf(
     Poll::Ready(Ok(n))
 }
 
+fn redact_error(mut error: reqwest::Error) -> reqwest::Error {
+    if let Some(url) = error.url_mut() {
+        if let Some(query) = url.query() {
+            if let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED") {
+                url.set_query(Some(redacted.as_str()));
+            }
+        }
+    }
+    error
+}
+
 impl http_client::HttpClient for ReqwestClient {
     fn proxy(&self) -> Option<&http::Uri> {
         self.proxy.as_ref()
@@ -217,7 +231,10 @@ impl http_client::HttpClient for ReqwestClient {
 
         let handle = self.handle.clone();
         async move {
-            let mut response = handle.spawn(async { request.send().await }).await??;
+            let mut response = handle
+                .spawn(async { request.send().await })
+                .await?
+                .map_err(redact_error)?;
 
             let headers = mem::take(response.headers_mut());
             let mut builder = http::Response::builder()