use std::fmt;
use decdn_protocol::APP_ERR_RATE_LIMITED;
use iroh::endpoint::{ApplicationClose, ConnectionError, ReadError, VarInt, WriteError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpstreamRateLimited {
pub label: Option<String>,
}
impl fmt::Display for UpstreamRateLimited {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.label {
Some(label) => write!(
f,
"upstream rate-limited (connection close, layer {label:?})"
),
None => write!(f, "upstream rate-limited (stream reset)"),
}
}
}
impl std::error::Error for UpstreamRateLimited {}
#[must_use]
pub(crate) fn rate_limit_shed(
err: &(dyn std::error::Error + 'static),
) -> Option<UpstreamRateLimited> {
let mut cursor: Option<&(dyn std::error::Error + 'static)> = Some(err);
while let Some(e) = cursor {
if let Some(hit) = shed_at(e) {
return Some(hit);
}
cursor = e.source();
}
None
}
fn shed_at(e: &(dyn std::error::Error + 'static)) -> Option<UpstreamRateLimited> {
if let Some(conn) = e.downcast_ref::<ConnectionError>() {
return closed_by_rate_limit(conn);
}
if let Some(read) = e.downcast_ref::<ReadError>() {
return match read {
ReadError::Reset(code) if is_rate_limit(*code) => {
Some(UpstreamRateLimited { label: None })
}
ReadError::ConnectionLost(conn) => closed_by_rate_limit(conn),
_ => None,
};
}
if let Some(write) = e.downcast_ref::<WriteError>() {
return match write {
WriteError::Stopped(code) if is_rate_limit(*code) => {
Some(UpstreamRateLimited { label: None })
}
WriteError::ConnectionLost(conn) => closed_by_rate_limit(conn),
_ => None,
};
}
if let Some(io) = e.downcast_ref::<std::io::Error>() {
return io.get_ref().and_then(|inner| rate_limit_shed(inner));
}
None
}
fn closed_by_rate_limit(conn: &ConnectionError) -> Option<UpstreamRateLimited> {
match conn {
ConnectionError::ApplicationClosed(ApplicationClose { error_code, reason })
if is_rate_limit(*error_code) =>
{
Some(UpstreamRateLimited {
label: Some(String::from_utf8_lossy(reason).into_owned()),
})
}
_ => None,
}
}
fn is_rate_limit(code: VarInt) -> bool {
code == VarInt::from_u32(APP_ERR_RATE_LIMITED)
}
pub(crate) fn transport_error<E>(stage: &'static str, err: E) -> anyhow::Error
where
E: std::error::Error + Send + Sync + 'static,
{
match rate_limit_shed(&err) {
Some(shed) => anyhow::Error::new(shed).context(stage),
None => anyhow::anyhow!("{stage}: {err}"),
}
}
#[cfg(test)]
mod tests;