Skip to main content

syslog_server_mcp/
rest_client.rs

1use crate::error::{Error, Result};
2use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue};
3use serde::de::DeserializeOwned;
4use std::time::Duration;
5use url::Url;
6
7const REST_TIMEOUT: Duration = Duration::from_secs(30);
8const RETRY_DELAYS: [Duration; 3] = [
9    Duration::from_millis(100),
10    Duration::from_millis(500),
11    Duration::from_secs(2),
12];
13
14#[derive(Clone)]
15pub struct RestClient {
16    base_url: Url,
17    inner: reqwest::Client,
18}
19
20impl RestClient {
21    pub fn new(
22        base_url: Url,
23        api_key: String,
24        insecure: bool,
25        mcp_client: Option<String>,
26    ) -> Result<Self> {
27        let mut headers = HeaderMap::new();
28        let mut auth_value = HeaderValue::from_str(&format!("Bearer {api_key}"))
29            .map_err(|e| Error::Config(format!("invalid api_key chars: {e}")))?;
30        auth_value.set_sensitive(true);
31        headers.insert(AUTHORIZATION, auth_value);
32
33        if let Some(name) = mcp_client {
34            let value = HeaderValue::from_str(&name)
35                .map_err(|e| Error::Config(format!("invalid mcp_client chars: {e}")))?;
36            headers.insert(HeaderName::from_static("mcp-client"), value);
37        }
38
39        let mut builder = reqwest::Client::builder()
40            .timeout(REST_TIMEOUT)
41            .default_headers(headers);
42        if insecure {
43            tracing::warn!("[WARN] SECURITY: TLS verification disabled (--insecure)");
44            builder = builder.danger_accept_invalid_certs(true);
45        }
46
47        Ok(Self {
48            base_url,
49            inner: builder.build()?,
50        })
51    }
52
53    pub async fn get_text(&self, path: &str) -> Result<String> {
54        let url = self
55            .base_url
56            .join(path)
57            .map_err(|e| Error::Programmer(format!("bad path {path}: {e}")))?;
58        let resp = self.inner.get(url).send().await?;
59        let status = resp.status();
60        if !status.is_success() {
61            let body = resp.text().await.unwrap_or_default();
62            return Err(status_error(status.as_u16(), path, body));
63        }
64        Ok(resp.text().await?)
65    }
66
67    /// Issue a single HEAD request and return only the response status.
68    pub async fn head(&self, path: &str) -> Result<reqwest::StatusCode> {
69        let url = self
70            .base_url
71            .join(path)
72            .map_err(|e| Error::Programmer(format!("bad path {path}: {e}")))?;
73        let resp = self.inner.head(url).send().await?;
74        Ok(resp.status())
75    }
76
77    pub async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
78        let url = self
79            .base_url
80            .join(path)
81            .map_err(|e| Error::Programmer(format!("bad path {path}: {e}")))?;
82
83        let mut last_err: Option<Error> = None;
84        for (attempt, delay) in std::iter::once(Duration::ZERO)
85            .chain(RETRY_DELAYS.iter().copied())
86            .enumerate()
87        {
88            if attempt > 0 {
89                tokio::time::sleep(delay).await;
90            }
91            let resp = self.inner.get(url.clone()).send().await?;
92            let status = resp.status();
93
94            if status.is_success() {
95                return Ok(resp.json().await?);
96            }
97            if status.as_u16() == 429 {
98                last_err = Some(Error::RetryExhausted(format!("rate limited on {url}")));
99                if let Some(retry_after) = resp
100                    .headers()
101                    .get("retry-after")
102                    .and_then(|v| v.to_str().ok())
103                    .and_then(|s| s.parse::<u64>().ok())
104                {
105                    tokio::time::sleep(Duration::from_secs(retry_after)).await;
106                } else {
107                    tokio::time::sleep(Duration::from_secs(1)).await;
108                }
109                continue;
110            }
111            if status.is_server_error() {
112                last_err = Some(Error::RetryExhausted(format!(
113                    "server returned {status} on {url}"
114                )));
115                continue;
116            }
117            // Non-retryable 4xx surfaces immediately; only 401/403 are auth failures.
118            let body = resp.text().await.unwrap_or_default();
119            return Err(status_error(status.as_u16(), url.as_str(), body));
120        }
121        Err(last_err.unwrap_or_else(|| Error::RetryExhausted("retries exhausted".into())))
122    }
123}
124
125fn status_error(status: u16, target: &str, body: String) -> Error {
126    let detail = if body.trim().is_empty() {
127        target.to_string()
128    } else {
129        format!("{target}: {body}")
130    };
131
132    match status {
133        401 | 403 => Error::Auth(format!("{status} on {detail}")),
134        _ => Error::Http {
135            status,
136            body: detail,
137        },
138    }
139}