From de1effbd5663bf0cd9adead9007e9971b445ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Houl=C3=A9?= <13155277+tomhoule@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:18:14 +0200 Subject: [PATCH] cloud_api_client: Add a few helpers for response handling (#58346) This centralizes some repeated logic across `cloud_api_client`, to map generic request failures (`ConnectionFailed`, 401s) to errors and read response bodies, handling failures there too. Closes CLO-759. Release Notes: - N/A --- Cargo.lock | 1 + crates/cloud_api_client/Cargo.toml | 1 + .../cloud_api_client/src/cloud_api_client.rs | 168 +++++------------- 3 files changed, 51 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 255d212129..b07750b7ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3194,6 +3194,7 @@ dependencies = [ "gpui_tokio", "http_client", "parking_lot", + "serde", "serde_json", "thiserror 2.0.17", "yawc", diff --git a/crates/cloud_api_client/Cargo.toml b/crates/cloud_api_client/Cargo.toml index 716276ccf4..baca3c1ce6 100644 --- a/crates/cloud_api_client/Cargo.toml +++ b/crates/cloud_api_client/Cargo.toml @@ -19,6 +19,7 @@ gpui.workspace = true gpui_tokio.workspace = true http_client.workspace = true parking_lot.workspace = true +serde.workspace = true serde_json.workspace = true async-lock.workspace = true thiserror.workspace = true diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index dc00d001d3..19504a7009 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -11,9 +11,10 @@ use gpui::{App, Task}; use gpui_tokio::Tokio; use http_client::http::request; use http_client::{ - AsyncBody, HttpClientWithUrl, HttpRequestExt, Json, Method, Request, StatusCode, + AsyncBody, HttpClientWithUrl, HttpRequestExt, Json, Method, Request, Response, StatusCode, }; use parking_lot::RwLock; +use serde::de::DeserializeOwned; use thiserror::Error; use yawc::WebSocket; @@ -108,7 +109,6 @@ impl CloudApiClient { &self, system_id: Option, ) -> Result { - let host = self.cloud_host(); let request_builder = Request::builder() .method(Method::GET) .uri( @@ -122,37 +122,7 @@ impl CloudApiClient { }); let request = self.build_request(request_builder, AsyncBody::default())?; - - let mut response = self.http_client.send(request).await.map_err(|source| { - ClientApiError::ConnectionFailed { - host: host.clone(), - source, - } - })?; - - if !response.status().is_success() { - if response.status() == StatusCode::UNAUTHORIZED { - return Err(ClientApiError::Unauthorized); - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await.ok(); - - return Err(ClientApiError::ServerError { - host, - status: response.status(), - body, - }); - } - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(|e| ClientApiError::InvalidResponse(e.into()))?; - - serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into())) + self.send_authenticated_json_request(request).await } pub fn connect(&self, cx: &App) -> Result>> { @@ -184,12 +154,11 @@ impl CloudApiClient { })) } - pub async fn create_llm_token( + async fn create_llm_token( &self, system_id: Option, organization_id: Option, ) -> Result { - let host = self.cloud_host(); let request_builder = Request::builder() .method(Method::POST) .uri( @@ -206,37 +175,7 @@ impl CloudApiClient { request_builder, Json(CreateLlmTokenBody { organization_id }), )?; - - let mut response = self.http_client.send(request).await.map_err(|source| { - ClientApiError::ConnectionFailed { - host: host.clone(), - source, - } - })?; - - if !response.status().is_success() { - if response.status() == StatusCode::UNAUTHORIZED { - return Err(ClientApiError::Unauthorized); - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await.ok(); - - return Err(ClientApiError::ServerError { - host, - status: response.status(), - body, - }); - } - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(|e| ClientApiError::InvalidResponse(e.into()))?; - - serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into())) + self.send_authenticated_json_request(request).await } pub async fn update_system_settings( @@ -244,7 +183,6 @@ impl CloudApiClient { system_id: String, body: UpdateSystemSettingsBody, ) -> Result { - let host = self.cloud_host(); let request_builder = Request::builder() .method(Method::PATCH) .uri( @@ -256,7 +194,22 @@ impl CloudApiClient { .header(ZED_SYSTEM_ID_HEADER_NAME, system_id); let request = self.build_request(request_builder, Json(body))?; + self.send_authenticated_json_request(request).await + } + async fn send_authenticated_json_request( + &self, + request: Request, + ) -> Result { + let mut response = self.send_authenticated_request(request).await?; + Self::read_response_json(&mut response).await + } + + async fn send_authenticated_request( + &self, + request: Request, + ) -> Result, ClientApiError> { + let host = self.cloud_host(); let mut response = self.http_client.send(request).await.map_err(|source| { ClientApiError::ConnectionFailed { host: host.clone(), @@ -264,29 +217,39 @@ impl CloudApiClient { } })?; - if !response.status().is_success() { - if response.status() == StatusCode::UNAUTHORIZED { - return Err(ClientApiError::Unauthorized); - } - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await.ok(); - - return Err(ClientApiError::ServerError { - host, - status: response.status(), - body, - }); + let status = response.status(); + if status.is_success() { + return Ok(response); } + if status == StatusCode::UNAUTHORIZED { + return Err(ClientApiError::Unauthorized); + } + + let body = match Self::read_response_body(&mut response).await { + Ok(body) => body, + Err(error) => format!("failed to read response body: {error}"), + }; + Err(ClientApiError::ServerError { host, status, body }) + } + + async fn read_response_json( + response: &mut Response, + ) -> Result { + let body = Self::read_response_body(response).await?; + serde_json::from_str(&body).map_err(|error| ClientApiError::InvalidResponse(error.into())) + } + + async fn read_response_body( + response: &mut Response, + ) -> Result { let mut body = String::new(); response .body_mut() .read_to_string(&mut body) .await - .map_err(|e| ClientApiError::InvalidResponse(e.into()))?; - - serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into())) + .map_err(|error| ClientApiError::InvalidResponse(error.into()))?; + Ok(body) } pub async fn validate_credentials(&self, user_id: u32, access_token: &str) -> Result { @@ -331,18 +294,7 @@ impl CloudApiClient { AsyncBody::from(serde_json::to_string(&body)?), )?; - let mut response = self.http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::bail!( - "Failed to submit agent feedback.\nStatus: {:?}\nBody: {body}", - response.status() - ) - } - + self.send_authenticated_request(request).await?; Ok(()) } @@ -359,18 +311,7 @@ impl CloudApiClient { AsyncBody::from(serde_json::to_string(&body)?), )?; - let mut response = self.http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::bail!( - "Failed to submit agent feedback comments.\nStatus: {:?}\nBody: {body}", - response.status() - ) - } - + self.send_authenticated_request(request).await?; Ok(()) } @@ -387,18 +328,7 @@ impl CloudApiClient { AsyncBody::from(serde_json::to_string(&body)?), )?; - let mut response = self.http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::bail!( - "Failed to submit edit prediction feedback.\nStatus: {:?}\nBody: {body}", - response.status() - ) - } - + self.send_authenticated_request(request).await?; Ok(()) } }