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
This commit is contained in:
Tom Houlé authored and GitHub committed 2026-06-03 09:18:14 +00:00
1 parent 2991e00e5e
commit de1effbd56
3 files changed
+51 -119

No files matched your search

Generated
+1
View File
@@ -3194,6 +3194,7 @@ dependencies = [
"gpui_tokio",
"http_client",
"parking_lot",
"serde",
"serde_json",
"thiserror 2.0.17",
"yawc",
+1
View File
@@ -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
+49 -119
View File
@@ -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<String>,
) -> Result<GetAuthenticatedUserResponse, ClientApiError> {
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<Task<Result<Connection>>> {
@@ -184,12 +154,11 @@ impl CloudApiClient {
}))
}
pub async fn create_llm_token(
async fn create_llm_token(
&self,
system_id: Option<String>,
organization_id: Option<OrganizationId>,
) -> Result<CreateLlmTokenResponse, ClientApiError> {
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<SystemSettings, ClientApiError> {
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<T: DeserializeOwned>(
&self,
request: Request<AsyncBody>,
) -> Result<T, ClientApiError> {
let mut response = self.send_authenticated_request(request).await?;
Self::read_response_json(&mut response).await
}
async fn send_authenticated_request(
&self,
request: Request<AsyncBody>,
) -> Result<Response<AsyncBody>, 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<T: DeserializeOwned>(
response: &mut Response<AsyncBody>,
) -> Result<T, ClientApiError> {
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<AsyncBody>,
) -> Result<String, ClientApiError> {
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<bool> {
@@ -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(())
}
}