systemprompt_cloud/api_client/
client.rs1use reqwest::{Client, StatusCode};
5use serde::de::DeserializeOwned;
6use systemprompt_models::net::{HTTP_CONNECT_TIMEOUT, HTTP_DEFAULT_TIMEOUT};
7
8use super::types::ApiError;
9use crate::error::{CloudError, CloudResult};
10
11#[derive(Debug)]
12pub struct CloudApiClient {
13 pub(super) client: Client,
14 pub(super) api_url: String,
15 pub(super) token: String,
16}
17
18impl CloudApiClient {
19 pub fn new(api_url: &str, token: &str) -> Result<Self, reqwest::Error> {
20 Ok(Self {
21 client: Client::builder()
22 .connect_timeout(HTTP_CONNECT_TIMEOUT)
23 .timeout(HTTP_DEFAULT_TIMEOUT)
24 .build()?,
25 api_url: api_url.to_string(),
26 token: token.to_string(),
27 })
28 }
29
30 #[must_use]
31 pub fn api_url(&self) -> &str {
32 &self.api_url
33 }
34
35 #[must_use]
36 pub fn token(&self) -> &str {
37 &self.token
38 }
39
40 pub(super) async fn handle_response<T: DeserializeOwned>(
41 &self,
42 response: reqwest::Response,
43 ) -> CloudResult<T> {
44 let status = response.status();
45
46 if status == StatusCode::UNAUTHORIZED {
47 return Err(CloudError::Unauthorized);
48 }
49
50 if !status.is_success() {
51 return Err(parse_error_response(status, response).await);
52 }
53
54 response.json().await.map_err(CloudError::from)
55 }
56
57 pub(super) async fn handle_no_content_response(
58 &self,
59 response: reqwest::Response,
60 ) -> CloudResult<()> {
61 let status = response.status();
62 if status == StatusCode::UNAUTHORIZED {
63 return Err(CloudError::Unauthorized);
64 }
65 if status == StatusCode::NO_CONTENT || status.is_success() {
66 return Ok(());
67 }
68 Err(parse_error_response(status, response).await)
69 }
70}
71
72pub(super) async fn parse_error_response(
73 status: StatusCode,
74 response: reqwest::Response,
75) -> CloudError {
76 let error_text = match response.text().await {
77 Ok(t) => t,
78 Err(e) => {
79 tracing::warn!(error = %e, "Failed to read error response body");
80 String::from("<failed to read response body>")
81 },
82 };
83
84 serde_json::from_str::<ApiError>(&error_text).map_or_else(
85 |_| CloudError::HttpStatus {
86 status: status.as_u16(),
87 body: error_text.chars().take(500).collect(),
88 },
89 |parsed| CloudError::ApiError {
90 message: format!("{}: {}", parsed.error.code, parsed.error.message),
91 },
92 )
93}