1use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use bytes::{Bytes, BytesMut};
18use reqwest::{Method, RequestBuilder};
19use serde::de::DeserializeOwned;
20use url::Url;
21
22use crate::api::HeaderPairs;
23use crate::swarm::{Error, RESPONSE_BODY_CAP, redact_url};
24
25pub const MAX_JSON_RESPONSE_BYTES: usize = 32 * 1024 * 1024;
33
34#[derive(Debug)]
36pub(crate) struct Inner {
37 pub(crate) base_url: Url,
38 pub(crate) http: reqwest::Client,
39}
40
41impl Inner {
42 pub(crate) fn url(&self, path: &str) -> Result<Url, Error> {
45 self.base_url
46 .join(path)
47 .map_err(|e| Error::argument(format!("invalid url: {e}")))
48 }
49
50 pub(crate) async fn send(&self, builder: RequestBuilder) -> Result<reqwest::Response, Error> {
59 let request = builder.build()?;
60 let method = request.method().to_string();
61 let redacted = redact_url(request.url());
67 let start = Instant::now();
68
69 let resp = self.http.execute(request).await?;
70 let elapsed_ms = start.elapsed().as_millis() as u64;
71 let status = resp.status().as_u16();
72
73 if resp.status().is_success() {
74 tracing::debug!(
75 target: "bee::http",
76 method = %method,
77 url = %redacted,
78 status,
79 elapsed_ms,
80 "bee api request"
81 );
82 return Ok(resp);
83 }
84 let status_text = format!(
85 "{status} {}",
86 resp.status().canonical_reason().unwrap_or("")
87 )
88 .trim_end()
89 .to_string();
90 let body = resp.bytes().await.map(|b| b.to_vec()).unwrap_or_default();
91 let n = body.len().min(RESPONSE_BODY_CAP);
92 tracing::debug!(
93 target: "bee::http",
94 method = %method,
95 url = %redacted,
96 status,
97 elapsed_ms,
98 body_len = body.len(),
99 "bee api error response"
100 );
101 Err(Error::Response {
102 method,
103 url: redacted,
104 status,
105 status_text,
106 body: body[..n].to_vec(),
107 })
108 }
109
110 pub(crate) async fn read_capped(
120 mut resp: reqwest::Response,
121 max_bytes: usize,
122 ) -> Result<Bytes, Error> {
123 if let Some(len) = resp.content_length() {
124 if len > max_bytes as u64 {
125 return Err(Error::argument(format!(
126 "response body exceeds limit ({len} > {max_bytes} bytes)"
127 )));
128 }
129 }
130 let mut buf = BytesMut::new();
131 while let Some(chunk) = resp.chunk().await? {
132 if buf.len() + chunk.len() > max_bytes {
133 return Err(Error::argument(format!(
134 "response body exceeds limit (>{max_bytes} bytes)"
135 )));
136 }
137 buf.extend_from_slice(&chunk);
138 }
139 Ok(buf.freeze())
140 }
141
142 pub(crate) async fn send_json<T: DeserializeOwned>(
145 &self,
146 builder: RequestBuilder,
147 ) -> Result<T, Error> {
148 let resp = self.send(builder).await?;
149 let bytes = Self::read_capped(resp, MAX_JSON_RESPONSE_BYTES).await?;
150 Ok(serde_json::from_slice(&bytes)?)
151 }
152
153 pub(crate) fn apply_headers(builder: RequestBuilder, headers: HeaderPairs) -> RequestBuilder {
155 let mut b = builder;
156 for (name, value) in headers {
157 b = b.header(name, value);
158 }
159 b
160 }
161}
162
163#[derive(Clone, Debug)]
165pub struct Client {
166 pub(crate) inner: Arc<Inner>,
167}
168
169impl Client {
170 pub fn new(url: &str) -> Result<Self, Error> {
174 let mut owned = url.to_owned();
175 if !owned.ends_with('/') {
176 owned.push('/');
177 }
178 let base_url =
179 Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
180 let http = reqwest::Client::builder()
181 .build()
182 .map_err(Error::Transport)?;
183 Ok(Self {
184 inner: Arc::new(Inner { base_url, http }),
185 })
186 }
187
188 pub fn with_http_client(url: &str, http: reqwest::Client) -> Result<Self, Error> {
192 let mut owned = url.to_owned();
193 if !owned.ends_with('/') {
194 owned.push('/');
195 }
196 let base_url =
197 Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
198 Ok(Self {
199 inner: Arc::new(Inner { base_url, http }),
200 })
201 }
202
203 pub fn with_token(url: &str, token: &str) -> Result<Self, Error> {
211 use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
212 let value = HeaderValue::from_str(&format!("Bearer {token}"))
213 .map_err(|e| Error::argument(format!("invalid token: {e}")))?;
214 let mut headers = HeaderMap::new();
215 headers.insert(AUTHORIZATION, value);
216 let http = reqwest::Client::builder()
217 .default_headers(headers)
218 .build()
219 .map_err(Error::Transport)?;
220 Self::with_http_client(url, http)
221 }
222
223 pub fn base_url(&self) -> &Url {
225 &self.inner.base_url
226 }
227
228 pub async fn ping(&self) -> Result<Duration, Error> {
232 let url = self.inner.url("health")?;
233 let builder = self.inner.http.request(Method::GET, url);
234 let start = Instant::now();
235 let _ = self.inner.send(builder).await?;
236 Ok(start.elapsed())
237 }
238
239 pub fn file(&self) -> crate::file::FileApi {
242 crate::file::FileApi::new(self.inner.clone())
243 }
244
245 pub fn postage(&self) -> crate::postage::PostageApi {
248 crate::postage::PostageApi::new(self.inner.clone())
249 }
250
251 pub fn debug(&self) -> crate::debug::DebugApi {
254 crate::debug::DebugApi::new(self.inner.clone())
255 }
256
257 pub fn api(&self) -> crate::api::ApiService {
260 crate::api::ApiService::new(self.inner.clone())
261 }
262
263 pub fn pss(&self) -> crate::pss::PssApi {
265 crate::pss::PssApi::new(self.inner.clone())
266 }
267
268 pub fn gsoc(&self) -> crate::gsoc::GsocApi {
270 crate::gsoc::GsocApi::new(self.inner.clone())
271 }
272}
273
274pub(crate) fn request(inner: &Inner, method: Method, path: &str) -> Result<RequestBuilder, Error> {
277 let url = inner.url(path)?;
278 Ok(inner.http.request(method, url))
279}