mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Expand ClientApiError with structured variants for connection, server, and response errors (#56214)
Self-Review Checklist: - [ ] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable Release Notes: - N/A
This commit is contained in:
parent
e621d0dd05
commit
7205fb9c71
2 changed files with 146 additions and 31 deletions
|
|
@ -28,10 +28,34 @@ struct Credentials {
|
|||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ClientApiError {
|
||||
/// 401 — credentials are invalid or expired.
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
/// No credentials have been set on the client.
|
||||
#[error("not signed in")]
|
||||
NotSignedIn,
|
||||
/// Connection-level failure: DNS, TCP, TLS, timeout, etc.
|
||||
/// The HTTP request never received a response.
|
||||
#[error("connection to {host} failed")]
|
||||
ConnectionFailed {
|
||||
host: String,
|
||||
#[source]
|
||||
source: anyhow::Error,
|
||||
},
|
||||
/// Server returned a non-success HTTP status (other than 401).
|
||||
#[error("{host} returned {status}")]
|
||||
ServerError {
|
||||
host: String,
|
||||
status: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
/// Failed to read or parse the response body after a successful HTTP status.
|
||||
#[error("invalid response")]
|
||||
InvalidResponse(#[source] anyhow::Error),
|
||||
/// Failed to build the HTTP request (URL construction, serialization, etc.).
|
||||
/// This typically indicates a programming error.
|
||||
#[error("failed to build request")]
|
||||
RequestBuildFailed(#[source] anyhow::Error),
|
||||
}
|
||||
|
||||
pub struct CloudApiClient {
|
||||
|
|
@ -62,25 +86,35 @@ impl CloudApiClient {
|
|||
*self.credentials.write() = None;
|
||||
}
|
||||
|
||||
fn cloud_host(&self) -> String {
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/")
|
||||
.ok()
|
||||
.and_then(|url| url.host_str().map(String::from))
|
||||
.unwrap_or_else(|| "cloud.zed.dev".into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: request::Builder,
|
||||
body: impl Into<AsyncBody>,
|
||||
) -> Result<Request<AsyncBody>> {
|
||||
) -> Result<Request<AsyncBody>, ClientApiError> {
|
||||
let credentials = self.credentials.read();
|
||||
let credentials = credentials.as_ref().context("no credentials provided")?;
|
||||
build_request(req, body, credentials)
|
||||
let credentials = credentials.as_ref().ok_or(ClientApiError::NotSignedIn)?;
|
||||
build_request(req, body, credentials).map_err(ClientApiError::RequestBuildFailed)
|
||||
}
|
||||
|
||||
pub async fn get_authenticated_user(
|
||||
&self,
|
||||
system_id: Option<String>,
|
||||
) -> Result<GetAuthenticatedUserResponse, ClientApiError> {
|
||||
let host = self.cloud_host();
|
||||
let request_builder = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/users/me")?
|
||||
.build_zed_cloud_url("/client/users/me")
|
||||
.map_err(ClientApiError::RequestBuildFailed)?
|
||||
.as_ref(),
|
||||
)
|
||||
.when_some(system_id, |builder, system_id| {
|
||||
|
|
@ -89,7 +123,12 @@ impl CloudApiClient {
|
|||
|
||||
let request = self.build_request(request_builder, AsyncBody::default())?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
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 {
|
||||
|
|
@ -97,16 +136,13 @@ impl CloudApiClient {
|
|||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.context("failed to read response body")?;
|
||||
response.body_mut().read_to_string(&mut body).await.ok();
|
||||
|
||||
return Err(ClientApiError::Other(anyhow::anyhow!(
|
||||
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)));
|
||||
return Err(ClientApiError::ServerError {
|
||||
host,
|
||||
status: response.status(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
|
|
@ -114,9 +150,9 @@ impl CloudApiClient {
|
|||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.context("failed to read response body")?;
|
||||
.map_err(|e| ClientApiError::InvalidResponse(e.into()))?;
|
||||
|
||||
Ok(serde_json::from_str(&body).context("failed to parse response body")?)
|
||||
serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into()))
|
||||
}
|
||||
|
||||
pub fn connect(&self, cx: &App) -> Result<Task<Result<Connection>>> {
|
||||
|
|
@ -153,11 +189,13 @@ impl CloudApiClient {
|
|||
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(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/llm_tokens")?
|
||||
.build_zed_cloud_url("/client/llm_tokens")
|
||||
.map_err(ClientApiError::RequestBuildFailed)?
|
||||
.as_ref(),
|
||||
)
|
||||
.when_some(system_id, |builder, system_id| {
|
||||
|
|
@ -169,7 +207,12 @@ impl CloudApiClient {
|
|||
Json(CreateLlmTokenBody { organization_id }),
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
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 {
|
||||
|
|
@ -177,16 +220,13 @@ impl CloudApiClient {
|
|||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.context("failed to read response body")?;
|
||||
response.body_mut().read_to_string(&mut body).await.ok();
|
||||
|
||||
return Err(ClientApiError::Other(anyhow::anyhow!(
|
||||
"Failed to create LLM token.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)));
|
||||
return Err(ClientApiError::ServerError {
|
||||
host,
|
||||
status: response.status(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
|
|
@ -194,9 +234,9 @@ impl CloudApiClient {
|
|||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.context("failed to read response body")?;
|
||||
.map_err(|e| ClientApiError::InvalidResponse(e.into()))?;
|
||||
|
||||
Ok(serde_json::from_str(&body).context("failed to parse response body")?)
|
||||
serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into()))
|
||||
}
|
||||
|
||||
pub async fn validate_credentials(&self, user_id: u32, access_token: &str) -> Result<bool> {
|
||||
|
|
|
|||
75
crates/reqwest_client/examples/download.rs
Normal file
75
crates/reqwest_client/examples/download.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use futures::AsyncReadExt as _;
|
||||
use http_client::{AsyncBody, HttpClient as _};
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let url = std::env::args()
|
||||
.nth(1)
|
||||
.ok_or_else(|| anyhow!("usage: cargo run -p reqwest_client --example download -- <url>"))?;
|
||||
|
||||
let client = ReqwestClient::user_agent("zed-reqwest-client-download-example")?;
|
||||
|
||||
futures::executor::block_on(async move {
|
||||
let started_at = Instant::now();
|
||||
let mut response = client
|
||||
.get(&url, AsyncBody::empty(), true)
|
||||
.await
|
||||
.with_context(|| format!("requesting {url}"))?;
|
||||
let headers_elapsed = started_at.elapsed();
|
||||
|
||||
println!("status: {}", response.status());
|
||||
println!("version: {:?}", response.version());
|
||||
if let Some(content_length) = response
|
||||
.headers()
|
||||
.get(http_client::http::header::CONTENT_LENGTH)
|
||||
&& let Ok(content_length) = content_length.to_str()
|
||||
{
|
||||
println!("content-length: {content_length}");
|
||||
}
|
||||
println!("time-to-headers: {:.3}s", headers_elapsed.as_secs_f64());
|
||||
|
||||
let mut buffer = vec![0; 1024 * 1024];
|
||||
let mut bytes_downloaded = 0usize;
|
||||
let body_started_at = Instant::now();
|
||||
|
||||
loop {
|
||||
let bytes_read = response
|
||||
.body_mut()
|
||||
.read(&mut buffer)
|
||||
.await
|
||||
.context("reading response body")?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
bytes_downloaded += bytes_read;
|
||||
}
|
||||
|
||||
let body_elapsed = body_started_at.elapsed();
|
||||
let total_elapsed = started_at.elapsed();
|
||||
let mebibytes = bytes_downloaded as f64 / 1024.0 / 1024.0;
|
||||
let body_seconds = body_elapsed.as_secs_f64();
|
||||
let total_seconds = total_elapsed.as_secs_f64();
|
||||
|
||||
println!("downloaded-bytes: {bytes_downloaded}");
|
||||
println!("downloaded-mib: {mebibytes:.3}");
|
||||
println!("body-time: {body_seconds:.3}s");
|
||||
println!("total-time: {total_seconds:.3}s");
|
||||
if body_seconds > 0.0 {
|
||||
println!("body-throughput: {:.3} MiB/s", mebibytes / body_seconds);
|
||||
}
|
||||
if total_seconds > 0.0 {
|
||||
println!("total-throughput: {:.3} MiB/s", mebibytes / total_seconds);
|
||||
}
|
||||
|
||||
anyhow::ensure!(
|
||||
response.status().is_success(),
|
||||
"download completed with unsuccessful status {}",
|
||||
response.status()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue