1use crate::Error;
4use reqwest::{Client, Method};
5use serde_json::Value;
6use std::time::Duration;
7
8pub struct HttpClient {
9 client: Client,
10 base_url: String,
11 service_key: String,
12 #[cfg_attr(not(test), allow(dead_code))]
13 timeout_ms: Option<u64>,
14}
15
16fn parse_timeout_ms(raw: Option<&str>) -> Option<u64> {
17 raw.and_then(|value| value.trim().parse::<u64>().ok())
18 .filter(|value| *value > 0)
19}
20
21impl HttpClient {
22 pub fn new(base_url: &str, service_key: &str) -> Result<Self, Error> {
23 let url = base_url.trim_end_matches('/').to_string();
24 let timeout_ms = parse_timeout_ms(std::env::var("EDGEBASE_HTTP_TIMEOUT_MS").ok().as_deref());
25 let mut builder = Client::builder();
26 if let Some(timeout_ms) = timeout_ms {
27 builder = builder.timeout(Duration::from_millis(timeout_ms));
28 }
29 Ok(Self {
30 client: builder.build()?,
31 base_url: url,
32 service_key: service_key.to_string(),
33 timeout_ms,
34 })
35 }
36
37 pub fn base_url(&self) -> &str {
38 &self.base_url
39 }
40
41 #[cfg_attr(not(test), allow(dead_code))]
42 pub(crate) fn timeout_ms(&self) -> Option<u64> {
43 self.timeout_ms
44 }
45
46 #[cfg_attr(not(test), allow(dead_code))]
47 pub(crate) fn parse_timeout_ms_for_tests(raw: Option<&str>) -> Option<u64> {
48 parse_timeout_ms(raw)
49 }
50
51 fn build_request(&self, method: Method, path: &str) -> reqwest::RequestBuilder {
52 let url = format!("{}{}", self.base_url, path);
53 let mut req = self.client.request(method, &url);
54 if !self.service_key.is_empty() {
55 req = req.header("X-EdgeBase-Service-Key", &self.service_key);
56 req = req.header("Authorization", format!("Bearer {}", self.service_key));
57 }
58 req
59 }
60
61 async fn send(&self, req: reqwest::RequestBuilder) -> Result<Value, Error> {
62 let resp = req.send().await?;
63 let status = resp.status();
64 let text = resp.text().await?;
65 if !status.is_success() {
66 let msg = serde_json::from_str::<Value>(&text)
67 .ok()
68 .and_then(|v| {
69 v.get("error")
70 .or_else(|| v.get("message"))
71 .and_then(|m| m.as_str())
72 .map(|s| s.to_string())
73 })
74 .unwrap_or_else(|| text.clone());
75 return Err(Error::Api {
76 status: status.as_u16(),
77 message: msg,
78 });
79 }
80 if text.is_empty() {
81 return Ok(Value::Null);
82 }
83 Ok(serde_json::from_str(&text)?)
84 }
85
86 pub async fn get(&self, path: &str) -> Result<Value, Error> {
87 let req = self.build_request(Method::GET, path);
88 self.send(req).await
89 }
90
91 pub async fn get_with_query(&self, path: &str, query: &std::collections::HashMap<String, String>) -> Result<Value, Error> {
92 let req = self.build_request(Method::GET, path).query(query);
93 self.send(req).await
94 }
95
96 pub async fn post(&self, path: &str, body: &Value) -> Result<Value, Error> {
97 let req = self.build_request(Method::POST, path).json(body);
98 self.send(req).await
99 }
100
101 pub async fn post_with_query(&self, path: &str, body: &Value, query: &std::collections::HashMap<String, String>) -> Result<Value, Error> {
102 let req = self.build_request(Method::POST, path).json(body).query(query);
103 self.send(req).await
104 }
105
106 pub async fn patch(&self, path: &str, body: &Value) -> Result<Value, Error> {
107 let req = self.build_request(Method::PATCH, path).json(body);
108 self.send(req).await
109 }
110
111 pub async fn delete(&self, path: &str) -> Result<Value, Error> {
112 let req = self.build_request(Method::DELETE, path);
113 self.send(req).await
114 }
115
116 pub async fn delete_with_body(&self, path: &str, body: &Value) -> Result<Value, Error> {
117 let req = self.build_request(Method::DELETE, path).json(body);
118 self.send(req).await
119 }
120
121 pub async fn head(&self, path: &str) -> Result<bool, Error> {
123 let req = self.build_request(Method::HEAD, path);
124 let resp = req.send().await?;
125 Ok(resp.status().is_success())
126 }
127
128 pub async fn put(&self, path: &str, body: &Value) -> Result<Value, Error> {
129 let req = self.build_request(Method::PUT, path).json(body);
130 self.send(req).await
131 }
132
133 pub async fn put_with_query(&self, path: &str, body: &Value, query: &std::collections::HashMap<String, String>) -> Result<Value, Error> {
134 let req = self.build_request(Method::PUT, path).json(body).query(query);
135 self.send(req).await
136 }
137
138 pub async fn upload_multipart(
140 &self, path: &str, key: &str, data: Vec<u8>, content_type: &str,
141 ) -> Result<Value, Error> {
142 use reqwest::multipart::{Form, Part};
143 let part = Part::bytes(data)
144 .file_name(key.to_string())
145 .mime_str(content_type)
146 .map_err(|e| Error::Url(e.to_string()))?;
147 let form = Form::new()
148 .part("file", part)
149 .text("key", key.to_string());
150 let url = format!("{}{}", self.base_url, path);
151 let mut req = self.client.post(&url).multipart(form);
152 if !self.service_key.is_empty() {
153 req = req.header("X-EdgeBase-Service-Key", &self.service_key);
154 req = req.header("Authorization", format!("Bearer {}", self.service_key));
155 }
156 self.send(req).await
157 }
158
159 pub async fn post_bytes(&self, path: &str, data: Vec<u8>, content_type: &str) -> Result<Value, Error> {
161 let req = self.build_request(Method::POST, path)
162 .header("Content-Type", content_type)
163 .body(data);
164 self.send(req).await
165 }
166
167 pub async fn download_raw(&self, path: &str) -> Result<Vec<u8>, Error> {
169 let req = self.build_request(Method::GET, path);
170 let resp = req.send().await?;
171 let status = resp.status();
172 if !status.is_success() {
173 let msg = resp.text().await.unwrap_or_default();
174 return Err(Error::Api { status: status.as_u16(), message: msg });
175 }
176 Ok(resp.bytes().await.map(|b| b.to_vec())?)
177 }
178}