@@ -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")?;
-
- return Err(ClientApiError::Other(anyhow::anyhow!(
- "Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
- response.status()
- )));
+ response.body_mut().read_to_string(&mut body).await.ok();
+
+ 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")?;
-
- return Err(ClientApiError::Other(anyhow::anyhow!(
- "Failed to create LLM token.\nStatus: {:?}\nBody: {body}",
- response.status()
- )));
+ response.body_mut().read_to_string(&mut body).await.ok();
+
+ 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> {
@@ -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(())
+ })
+}