Skip to main content

bee/
client.rs

1//! Top-level [`Client`] and the shared [`Inner`] HTTP plumbing.
2//!
3//! Mirrors bee-go's `bee.Client`. A `Client` is cheaply cloneable
4//! (`Arc<Inner>`) and yields per-domain handles via accessors:
5//!
6//! ```no_run
7//! # use bee::Client;
8//! # async fn run() -> Result<(), bee::Error> {
9//! let client = Client::new("http://localhost:1633")?;
10//! let health = client.debug().health().await?;
11//! # Ok(()) }
12//! ```
13
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use reqwest::{Method, RequestBuilder};
18use serde::de::DeserializeOwned;
19use url::Url;
20
21use crate::api::HeaderPairs;
22use crate::swarm::{Error, RESPONSE_BODY_CAP};
23
24/// Shared HTTP/state used by every sub-service.
25#[derive(Debug)]
26pub(crate) struct Inner {
27    pub(crate) base_url: Url,
28    pub(crate) http: reqwest::Client,
29}
30
31impl Inner {
32    /// Resolve a path against the base URL. The base URL is normalized
33    /// to end with `/`, so `path` is treated as relative.
34    pub(crate) fn url(&self, path: &str) -> Result<Url, Error> {
35        self.base_url
36            .join(path)
37            .map_err(|e| Error::argument(format!("invalid url: {e}")))
38    }
39
40    /// Build a request, send it, and translate non-2xx responses into
41    /// [`Error::Response`] with method / URL / capped body captured.
42    ///
43    /// Emits `tracing::debug!` events at target `bee::http` carrying
44    /// `method`, `url`, `status`, and `elapsed_ms` for every request.
45    /// Subscribe with `RUST_LOG=bee::http=debug` (or any subscriber
46    /// that captures spans/events) to surface live API traffic — the
47    /// bee-tui command-log pane uses this.
48    pub(crate) async fn send(&self, builder: RequestBuilder) -> Result<reqwest::Response, Error> {
49        let request = builder.build()?;
50        let method = request.method().to_string();
51        let url = request.url().to_string();
52        let start = Instant::now();
53
54        let resp = self.http.execute(request).await?;
55        let elapsed_ms = start.elapsed().as_millis() as u64;
56        let status = resp.status().as_u16();
57
58        if resp.status().is_success() {
59            tracing::debug!(
60                target: "bee::http",
61                method = %method,
62                url = %url,
63                status,
64                elapsed_ms,
65                "bee api request"
66            );
67            return Ok(resp);
68        }
69        let status_text = format!(
70            "{status} {}",
71            resp.status().canonical_reason().unwrap_or("")
72        )
73        .trim_end()
74        .to_string();
75        let body = resp.bytes().await.map(|b| b.to_vec()).unwrap_or_default();
76        let n = body.len().min(RESPONSE_BODY_CAP);
77        tracing::debug!(
78            target: "bee::http",
79            method = %method,
80            url = %url,
81            status,
82            elapsed_ms,
83            body_len = body.len(),
84            "bee api error response"
85        );
86        Err(Error::Response {
87            method,
88            url,
89            status,
90            status_text,
91            body: body[..n].to_vec(),
92        })
93    }
94
95    /// Send and parse the response body as JSON.
96    pub(crate) async fn send_json<T: DeserializeOwned>(
97        &self,
98        builder: RequestBuilder,
99    ) -> Result<T, Error> {
100        let resp = self.send(builder).await?;
101        let bytes = resp.bytes().await?;
102        Ok(serde_json::from_slice(&bytes)?)
103    }
104
105    /// Apply a list of header pairs to a request builder.
106    pub(crate) fn apply_headers(builder: RequestBuilder, headers: HeaderPairs) -> RequestBuilder {
107        let mut b = builder;
108        for (name, value) in headers {
109            b = b.header(name, value);
110        }
111        b
112    }
113}
114
115/// Top-level Bee API client.
116#[derive(Clone, Debug)]
117pub struct Client {
118    pub(crate) inner: Arc<Inner>,
119}
120
121impl Client {
122    /// Construct a client from a base URL (e.g. `"http://localhost:1633"`).
123    /// A trailing slash is appended if missing so relative paths resolve
124    /// correctly.
125    pub fn new(url: &str) -> Result<Self, Error> {
126        let mut owned = url.to_owned();
127        if !owned.ends_with('/') {
128            owned.push('/');
129        }
130        let base_url =
131            Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
132        let http = reqwest::Client::builder()
133            .build()
134            .map_err(Error::Transport)?;
135        Ok(Self {
136            inner: Arc::new(Inner { base_url, http }),
137        })
138    }
139
140    /// Construct a client with a caller-provided [`reqwest::Client`].
141    /// Use this to share a connection pool with other code or to set
142    /// custom timeouts / TLS roots.
143    pub fn with_http_client(url: &str, http: reqwest::Client) -> Result<Self, Error> {
144        let mut owned = url.to_owned();
145        if !owned.ends_with('/') {
146            owned.push('/');
147        }
148        let base_url =
149            Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
150        Ok(Self {
151            inner: Arc::new(Inner { base_url, http }),
152        })
153    }
154
155    /// Construct a client that sends `Authorization: Bearer <token>`
156    /// on every request. Convenience for talking to a Bee node running
157    /// with restricted-mode auth.
158    ///
159    /// For more control (custom timeouts, TLS roots, additional
160    /// headers), build a [`reqwest::Client`] yourself and pass it via
161    /// [`Client::with_http_client`].
162    pub fn with_token(url: &str, token: &str) -> Result<Self, Error> {
163        use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
164        let value = HeaderValue::from_str(&format!("Bearer {token}"))
165            .map_err(|e| Error::argument(format!("invalid token: {e}")))?;
166        let mut headers = HeaderMap::new();
167        headers.insert(AUTHORIZATION, value);
168        let http = reqwest::Client::builder()
169            .default_headers(headers)
170            .build()
171            .map_err(Error::Transport)?;
172        Self::with_http_client(url, http)
173    }
174
175    /// Borrow the configured base URL.
176    pub fn base_url(&self) -> &Url {
177        &self.inner.base_url
178    }
179
180    /// `GET /health` round-trip latency. Useful for connection-status
181    /// indicators in dashboards and TUIs. Returns the elapsed
182    /// [`Duration`] regardless of body — the response is not parsed.
183    pub async fn ping(&self) -> Result<Duration, Error> {
184        let url = self.inner.url("health")?;
185        let builder = self.inner.http.request(Method::GET, url);
186        let start = Instant::now();
187        let _ = self.inner.send(builder).await?;
188        Ok(start.elapsed())
189    }
190
191    /// Sub-service: file / data / chunk / SOC / feed / collection
192    /// uploads and downloads.
193    pub fn file(&self) -> crate::file::FileApi {
194        crate::file::FileApi::new(self.inner.clone())
195    }
196
197    /// Sub-service: postage batch CRUD + stamp metadata. Stamp math
198    /// helpers live as free functions in [`crate::postage`].
199    pub fn postage(&self) -> crate::postage::PostageApi {
200        crate::postage::PostageApi::new(self.inner.clone())
201    }
202
203    /// Sub-service: debug / operator endpoints (health, versions,
204    /// peers, accounting, chequebook, stake).
205    pub fn debug(&self) -> crate::debug::DebugApi {
206        crate::debug::DebugApi::new(self.inner.clone())
207    }
208
209    /// Sub-service: generic `/api/*` endpoints (pin, tag, stewardship,
210    /// grantee, envelope).
211    pub fn api(&self) -> crate::api::ApiService {
212        crate::api::ApiService::new(self.inner.clone())
213    }
214
215    /// Sub-service: PSS send + websocket subscribe / receive.
216    pub fn pss(&self) -> crate::pss::PssApi {
217        crate::pss::PssApi::new(self.inner.clone())
218    }
219
220    /// Sub-service: GSOC send + websocket subscribe.
221    pub fn gsoc(&self) -> crate::gsoc::GsocApi {
222        crate::gsoc::GsocApi::new(self.inner.clone())
223    }
224}
225
226/// Shorthand: build a `RequestBuilder` for `(method, path)` against
227/// the inner HTTP client.
228pub(crate) fn request(inner: &Inner, method: Method, path: &str) -> Result<RequestBuilder, Error> {
229    let url = inner.url(path)?;
230    Ok(inner.http.request(method, url))
231}