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