use std::fmt;
use std::time::Duration;
use bevy_ecs::resource::Resource;
use http::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
pub const DEFAULT_WORKERS: usize = 2;
pub const MAX_WORKERS: usize = 8;
pub const DEFAULT_MAX_BODY_BYTES: u64 = 10 * 1024 * 1024;
pub const MAX_TIMEOUT: Duration = Duration::from_secs(3600);
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConfigError {
NoBaseUrl,
BadBaseUrl(String),
BadHeader(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::NoBaseUrl => f.write_str("no base URL is configured"),
ConfigError::BadBaseUrl(why) => write!(f, "bad base URL: {why}"),
ConfigError::BadHeader(why) => write!(f, "bad default header: {why}"),
}
}
}
impl std::error::Error for ConfigError {}
#[derive(Resource, Clone)]
pub struct HttpConfig {
base_url: String,
timeout: Duration,
headers: HeaderMap,
header_errors: std::collections::BTreeMap<String, String>,
workers: usize,
allow_insecure_http: bool,
max_body_bytes: u64,
}
impl Default for HttpConfig {
fn default() -> Self {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(concat!("bevy_net_backend/", env!("CARGO_PKG_VERSION"))));
Self {
base_url: String::new(),
timeout: DEFAULT_TIMEOUT,
headers,
header_errors: std::collections::BTreeMap::new(),
workers: DEFAULT_WORKERS,
allow_insecure_http: false,
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
}
}
}
impl fmt::Debug for HttpConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let header_names: Vec<&str> = self.headers.keys().map(HeaderName::as_str).collect();
f.debug_struct("HttpConfig")
.field("base_url", &crate::request::parse_base(&self.base_url).map(|b| format!("{}{}", b.origin, b.prefix)).unwrap_or_else(|_| "<invalid>".into()))
.field("timeout", &self.timeout)
.field("headers", &header_names)
.field("workers", &self.workers)
.field("allow_insecure_http", &self.allow_insecure_http)
.field("max_body_bytes", &self.max_body_bytes)
.finish()
}
}
impl HttpConfig {
pub fn new(base_url: impl Into<String>) -> Self {
Self { base_url: base_url.into(), ..Self::default() }
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn set_base_url(&mut self, base_url: impl Into<String>) {
self.base_url = base_url.into();
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = clamp_timeout(timeout);
self
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = clamp_timeout(timeout);
}
pub fn with_header(mut self, name: &str, value: &str) -> Self {
let key = name.to_ascii_lowercase();
match (HeaderName::try_from(name), HeaderValue::try_from(value)) {
(Ok(name), Ok(value)) => {
self.headers.insert(name, value);
self.header_errors.remove(&key);
}
(Err(_), _) => {
self.header_errors.insert(key, format!("`{name}` is not a valid header name"));
}
(Ok(_), Err(_)) => {
self.header_errors.insert(key, format!("the value of `{name}` is not a valid header value"));
}
}
self
}
pub fn without_header(mut self, name: &str) -> Self {
self.headers.remove(name);
self.header_errors.remove(&name.to_ascii_lowercase());
self
}
pub fn with_workers(mut self, workers: usize) -> Self {
self.workers = workers.clamp(1, MAX_WORKERS);
self
}
pub fn allow_insecure_http(mut self, allow: bool) -> Self {
self.allow_insecure_http = allow;
self
}
pub fn with_max_body_bytes(mut self, bytes: u64) -> Self {
self.max_body_bytes = bytes.max(1);
self
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn workers(&self) -> usize {
self.workers
}
pub fn insecure_http_allowed(&self) -> bool {
self.allow_insecure_http
}
pub fn max_body_bytes(&self) -> u64 {
self.max_body_bytes
}
pub fn validate(&self) -> Result<(), ConfigError> {
if let Some(why) = self.header_errors.values().next() {
return Err(ConfigError::BadHeader(why.clone()));
}
crate::request::parse_base(&self.base_url).map(|_| ())
}
}
fn clamp_timeout(timeout: Duration) -> Duration {
timeout.clamp(Duration::from_millis(1), MAX_TIMEOUT)
}