syslog_server_mcp/
rest_client.rs1use 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 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 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}