use std::{ffi::OsString, fmt, time::Duration};
use crate::{Result, error::AnytypeError};
pub const ANYTYPE_HTTP_TIMEOUT_SECS: &str = "ANYTYPE_HTTP_TIMEOUT_SECS";
pub const MAX_HTTP_TIMEOUT: Duration = Duration::from_secs(3_600);
pub const DEFAULT_STANDARD_HTTP_TIMEOUT: Duration = Duration::from_secs(120);
pub const DEFAULT_LONG_HTTP_TIMEOUT: Duration = Duration::from_secs(600);
pub const DEFAULT_SSE_OPEN_TIMEOUT: Duration = Duration::from_secs(120);
pub const DEFAULT_SSE_ERROR_BODY_TIMEOUT: Duration = Duration::from_secs(120);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HttpTimeoutPolicy {
pub standard_operation: Option<Duration>,
pub long_operation: Option<Duration>,
pub sse_open: Option<Duration>,
pub sse_error_body: Option<Duration>,
pub sse_idle: Option<Duration>,
pub sse_total_lifetime: Option<Duration>,
}
impl Default for HttpTimeoutPolicy {
fn default() -> Self {
Self {
standard_operation: Some(DEFAULT_STANDARD_HTTP_TIMEOUT),
long_operation: Some(DEFAULT_LONG_HTTP_TIMEOUT),
sse_open: Some(DEFAULT_SSE_OPEN_TIMEOUT),
sse_error_body: Some(DEFAULT_SSE_ERROR_BODY_TIMEOUT),
sse_idle: None,
sse_total_lifetime: None,
}
}
}
impl HttpTimeoutPolicy {
pub(crate) fn validate(self) -> Result<Self> {
for (field, value) in [
("standard_operation", self.standard_operation),
("long_operation", self.long_operation),
("sse_open", self.sse_open),
("sse_error_body", self.sse_error_body),
("sse_idle", self.sse_idle),
("sse_total_lifetime", self.sse_total_lifetime),
] {
if let Some(duration) = value
&& !(Duration::from_secs(1)..=MAX_HTTP_TIMEOUT).contains(&duration)
{
return Err(AnytypeError::Validation {
message: format!(
"http_timeouts.{field} must be disabled or between 1 and 3600 seconds"
),
});
}
}
Ok(self)
}
pub(crate) fn resolve(explicit: Option<Self>) -> Result<Self> {
if let Some(policy) = explicit {
return policy.validate();
}
Self::from_environment(std::env::var_os(ANYTYPE_HTTP_TIMEOUT_SECS))
}
fn from_environment(value: Option<OsString>) -> Result<Self> {
let Some(value) = value else {
return Ok(Self::default());
};
let value = value.into_string().map_err(|_| AnytypeError::Validation {
message: format!("{ANYTYPE_HTTP_TIMEOUT_SECS} must be Unicode ASCII decimal"),
})?;
if value.is_empty()
|| !value.bytes().all(|byte| byte.is_ascii_digit())
|| (value.len() > 1 && value.starts_with('0'))
{
return Err(AnytypeError::Validation {
message: format!(
"{ANYTYPE_HTTP_TIMEOUT_SECS} must be an ASCII decimal from 0 through 3600"
),
});
}
let seconds = value.parse::<u64>().map_err(|_| AnytypeError::Validation {
message: format!(
"{ANYTYPE_HTTP_TIMEOUT_SECS} must be an ASCII decimal from 0 through 3600"
),
})?;
if seconds > MAX_HTTP_TIMEOUT.as_secs() {
return Err(AnytypeError::Validation {
message: format!("{ANYTYPE_HTTP_TIMEOUT_SECS} must not exceed 3600 seconds"),
});
}
let inherited = (seconds != 0).then(|| Duration::from_secs(seconds));
Ok(Self {
standard_operation: inherited,
long_operation: inherited,
sse_open: inherited,
sse_error_body: inherited,
sse_idle: None,
sse_total_lifetime: None,
})
}
pub(crate) const fn duration(self, class: HttpTimeoutClass) -> Option<Duration> {
match class {
HttpTimeoutClass::StandardOperation => self.standard_operation,
HttpTimeoutClass::LongOperation => self.long_operation,
HttpTimeoutClass::SseOpen => self.sse_open,
HttpTimeoutClass::SseErrorBody => self.sse_error_body,
HttpTimeoutClass::SseIdle => self.sse_idle,
HttpTimeoutClass::SseLifetime => self.sse_total_lifetime,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HttpTimeoutClass {
StandardOperation,
LongOperation,
SseOpen,
SseErrorBody,
SseIdle,
SseLifetime,
}
impl HttpTimeoutClass {
pub(crate) const COUNT: usize = 6;
pub(crate) const fn index(self) -> usize {
match self {
Self::StandardOperation => 0,
Self::LongOperation => 1,
Self::SseOpen => 2,
Self::SseErrorBody => 3,
Self::SseIdle => 4,
Self::SseLifetime => 5,
}
}
}
impl fmt::Display for HttpTimeoutClass {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::StandardOperation => "standard_operation",
Self::LongOperation => "long_operation",
Self::SseOpen => "sse_open",
Self::SseErrorBody => "sse_error_body",
Self::SseIdle => "sse_idle",
Self::SseLifetime => "sse_lifetime",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimeoutOutcome {
ReadAborted,
MutationIndeterminate,
StreamTerminated,
}
impl TimeoutOutcome {
pub(crate) const COUNT: usize = 3;
pub(crate) const fn index(self) -> usize {
match self {
Self::ReadAborted => 0,
Self::MutationIndeterminate => 1,
Self::StreamTerminated => 2,
}
}
}
impl fmt::Display for TimeoutOutcome {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::ReadAborted => "read_aborted",
Self::MutationIndeterminate => "mutation_indeterminate",
Self::StreamTerminated => "stream_terminated",
})
}
}
pub(crate) fn timeout_outcome(method: &reqwest::Method) -> TimeoutOutcome {
if matches!(
*method,
reqwest::Method::GET | reqwest::Method::HEAD | reqwest::Method::OPTIONS
) {
TimeoutOutcome::ReadAborted
} else {
TimeoutOutcome::MutationIndeterminate
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_profile_values_are_pinned_to_policy_literals() {
let policy = HttpTimeoutPolicy::default();
assert_eq!(policy.standard_operation, Some(Duration::from_secs(120)));
assert_eq!(policy.long_operation, Some(Duration::from_secs(600)));
assert_eq!(policy.sse_open, Some(Duration::from_secs(120)));
assert_eq!(policy.sse_error_body, Some(Duration::from_secs(120)));
assert_eq!(policy.sse_idle, None);
assert_eq!(policy.sse_total_lifetime, None);
assert_eq!(DEFAULT_STANDARD_HTTP_TIMEOUT, Duration::from_secs(120));
assert_eq!(DEFAULT_LONG_HTTP_TIMEOUT, Duration::from_secs(600));
assert_eq!(DEFAULT_SSE_OPEN_TIMEOUT, Duration::from_secs(120));
assert_eq!(DEFAULT_SSE_ERROR_BODY_TIMEOUT, Duration::from_secs(120));
assert_eq!(MAX_HTTP_TIMEOUT, Duration::from_secs(3_600));
}
#[test]
fn inherited_environment_contract_is_exact() {
assert_eq!(
HttpTimeoutPolicy::from_environment(None).expect("defaults"),
HttpTimeoutPolicy::default()
);
let disabled = HttpTimeoutPolicy::from_environment(Some(OsString::from("0")))
.expect("disabled policy");
assert_eq!(disabled.standard_operation, None);
assert_eq!(disabled.long_operation, None);
assert_eq!(disabled.sse_open, None);
assert_eq!(disabled.sse_error_body, None);
assert_eq!(disabled.sse_idle, None);
assert_eq!(disabled.sse_total_lifetime, None);
let finite =
HttpTimeoutPolicy::from_environment(Some(OsString::from("17"))).expect("finite policy");
assert_eq!(finite.standard_operation, Some(Duration::from_secs(17)));
assert_eq!(finite.long_operation, Some(Duration::from_secs(17)));
}
#[test]
fn inherited_environment_rejects_non_decimal_and_out_of_range_values() {
for value in [
"",
"00",
"000",
"01",
"001",
" 1",
"+1",
"-1",
"1.0",
"3601",
"18446744073709551616",
] {
assert!(HttpTimeoutPolicy::from_environment(Some(OsString::from(value))).is_err());
}
}
#[cfg(unix)]
#[test]
fn inherited_environment_rejects_non_unicode() {
use std::os::unix::ffi::OsStringExt;
assert!(HttpTimeoutPolicy::from_environment(Some(OsString::from_vec(vec![0xff]))).is_err());
}
#[test]
fn explicit_policy_rejects_zero_subsecond_and_over_maximum_values() {
for duration in [
Duration::ZERO,
Duration::from_millis(999),
Duration::from_secs(3_601),
] {
let policy = HttpTimeoutPolicy {
standard_operation: Some(duration),
..HttpTimeoutPolicy::default()
};
assert!(policy.validate().is_err());
}
let disabled = HttpTimeoutPolicy {
standard_operation: None,
..HttpTimeoutPolicy::default()
};
assert!(disabled.validate().is_ok());
}
#[test]
fn explicit_policy_resolution_ignores_environment_resolution() {
let explicit = HttpTimeoutPolicy {
standard_operation: None,
long_operation: Some(Duration::from_secs(17)),
sse_open: None,
sse_error_body: Some(Duration::from_secs(18)),
sse_idle: Some(Duration::from_secs(19)),
sse_total_lifetime: None,
};
assert_eq!(
HttpTimeoutPolicy::resolve(Some(explicit)).expect("explicit policy"),
explicit
);
}
}