use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::{Bytes, BytesMut};
use reqwest::{Method, RequestBuilder};
use serde::de::DeserializeOwned;
use url::Url;
use crate::api::HeaderPairs;
use crate::swarm::{Error, RESPONSE_BODY_CAP, redact_url};
pub const MAX_JSON_RESPONSE_BYTES: usize = 32 * 1024 * 1024;
#[derive(Debug)]
pub(crate) struct Inner {
pub(crate) base_url: Url,
pub(crate) http: reqwest::Client,
}
impl Inner {
pub(crate) fn url(&self, path: &str) -> Result<Url, Error> {
self.base_url
.join(path)
.map_err(|e| Error::argument(format!("invalid url: {e}")))
}
pub(crate) async fn send(&self, builder: RequestBuilder) -> Result<reqwest::Response, Error> {
let request = builder.build()?;
let method = request.method().to_string();
let redacted = redact_url(request.url());
let start = Instant::now();
let resp = self.http.execute(request).await?;
let elapsed_ms = start.elapsed().as_millis() as u64;
let status = resp.status().as_u16();
if resp.status().is_success() {
tracing::debug!(
target: "bee::http",
method = %method,
url = %redacted,
status,
elapsed_ms,
"bee api request"
);
return Ok(resp);
}
let status_text = format!(
"{status} {}",
resp.status().canonical_reason().unwrap_or("")
)
.trim_end()
.to_string();
let body = resp.bytes().await.map(|b| b.to_vec()).unwrap_or_default();
let n = body.len().min(RESPONSE_BODY_CAP);
tracing::debug!(
target: "bee::http",
method = %method,
url = %redacted,
status,
elapsed_ms,
body_len = body.len(),
"bee api error response"
);
Err(Error::Response {
method,
url: redacted,
status,
status_text,
body: body[..n].to_vec(),
})
}
pub(crate) async fn read_capped(
mut resp: reqwest::Response,
max_bytes: usize,
) -> Result<Bytes, Error> {
if let Some(len) = resp.content_length() {
if len > max_bytes as u64 {
return Err(Error::argument(format!(
"response body exceeds limit ({len} > {max_bytes} bytes)"
)));
}
}
let mut buf = BytesMut::new();
while let Some(chunk) = resp.chunk().await? {
if buf.len() + chunk.len() > max_bytes {
return Err(Error::argument(format!(
"response body exceeds limit (>{max_bytes} bytes)"
)));
}
buf.extend_from_slice(&chunk);
}
Ok(buf.freeze())
}
pub(crate) async fn send_json<T: DeserializeOwned>(
&self,
builder: RequestBuilder,
) -> Result<T, Error> {
let resp = self.send(builder).await?;
let bytes = Self::read_capped(resp, MAX_JSON_RESPONSE_BYTES).await?;
Ok(serde_json::from_slice(&bytes)?)
}
pub(crate) fn apply_headers(builder: RequestBuilder, headers: HeaderPairs) -> RequestBuilder {
let mut b = builder;
for (name, value) in headers {
b = b.header(name, value);
}
b
}
}
#[derive(Clone, Debug)]
pub struct Client {
pub(crate) inner: Arc<Inner>,
}
impl Client {
pub fn new(url: &str) -> Result<Self, Error> {
let mut owned = url.to_owned();
if !owned.ends_with('/') {
owned.push('/');
}
let base_url =
Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
let http = reqwest::Client::builder()
.build()
.map_err(Error::Transport)?;
Ok(Self {
inner: Arc::new(Inner { base_url, http }),
})
}
pub fn with_http_client(url: &str, http: reqwest::Client) -> Result<Self, Error> {
let mut owned = url.to_owned();
if !owned.ends_with('/') {
owned.push('/');
}
let base_url =
Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
Ok(Self {
inner: Arc::new(Inner { base_url, http }),
})
}
pub fn with_token(url: &str, token: &str) -> Result<Self, Error> {
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
let value = HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|e| Error::argument(format!("invalid token: {e}")))?;
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, value);
let http = reqwest::Client::builder()
.default_headers(headers)
.build()
.map_err(Error::Transport)?;
Self::with_http_client(url, http)
}
pub fn base_url(&self) -> &Url {
&self.inner.base_url
}
pub async fn ping(&self) -> Result<Duration, Error> {
let url = self.inner.url("health")?;
let builder = self.inner.http.request(Method::GET, url);
let start = Instant::now();
let _ = self.inner.send(builder).await?;
Ok(start.elapsed())
}
pub fn file(&self) -> crate::file::FileApi {
crate::file::FileApi::new(self.inner.clone())
}
pub fn postage(&self) -> crate::postage::PostageApi {
crate::postage::PostageApi::new(self.inner.clone())
}
pub fn debug(&self) -> crate::debug::DebugApi {
crate::debug::DebugApi::new(self.inner.clone())
}
pub fn api(&self) -> crate::api::ApiService {
crate::api::ApiService::new(self.inner.clone())
}
pub fn pss(&self) -> crate::pss::PssApi {
crate::pss::PssApi::new(self.inner.clone())
}
pub fn gsoc(&self) -> crate::gsoc::GsocApi {
crate::gsoc::GsocApi::new(self.inner.clone())
}
}
pub(crate) fn request(inner: &Inner, method: Method, path: &str) -> Result<RequestBuilder, Error> {
let url = inner.url(path)?;
Ok(inner.http.request(method, url))
}