#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
DnsError(String),
ConnectionError(String),
Timeout {
timeout_ms: Option<u64>,
},
TlsError(String),
InvalidUrl(String),
InvalidRequest(String),
ResponseError(String),
SsrfBlocked(String),
Other(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DnsError(msg) => write!(f, "dns error: {msg}"),
Self::ConnectionError(msg) => write!(f, "connection error: {msg}"),
Self::Timeout { timeout_ms: None } => write!(f, "request timeout"),
Self::Timeout {
timeout_ms: Some(ms),
} => write!(f, "request timeout after {ms}ms"),
Self::TlsError(msg) => write!(f, "tls error: {msg}"),
Self::InvalidUrl(msg) => write!(f, "invalid url: {msg}"),
Self::InvalidRequest(msg) => write!(f, "invalid request: {msg}"),
Self::ResponseError(msg) => write!(f, "response error: {msg}"),
Self::SsrfBlocked(msg) => write!(f, "ssrf blocked: {msg}"),
Self::Other(msg) => write!(f, "http client error: {msg}"),
}
}
}
impl std::error::Error for Error {}
impl Error {
#[inline]
#[must_use]
pub fn dns(msg: impl Into<String>) -> Self {
Self::DnsError(msg.into())
}
#[inline]
#[must_use]
pub fn connection(msg: impl Into<String>) -> Self {
Self::ConnectionError(msg.into())
}
#[inline]
#[must_use]
pub const fn timeout() -> Self {
Self::Timeout { timeout_ms: None }
}
#[inline]
#[must_use]
pub const fn timeout_with_duration(ms: u64) -> Self {
Self::Timeout {
timeout_ms: Some(ms),
}
}
#[inline]
#[must_use]
pub fn tls(msg: impl Into<String>) -> Self {
Self::TlsError(msg.into())
}
#[inline]
#[must_use]
pub fn invalid_url(msg: impl Into<String>) -> Self {
Self::InvalidUrl(msg.into())
}
#[inline]
#[must_use]
pub fn invalid_request(msg: impl Into<String>) -> Self {
Self::InvalidRequest(msg.into())
}
#[inline]
#[must_use]
pub fn response(msg: impl Into<String>) -> Self {
Self::ResponseError(msg.into())
}
#[inline]
#[must_use]
pub fn ssrf_blocked(msg: impl Into<String>) -> Self {
Self::SsrfBlocked(msg.into())
}
#[inline]
#[must_use]
pub fn other(msg: impl Into<String>) -> Self {
Self::Other(msg.into())
}
#[inline]
#[must_use]
pub const fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout { .. })
}
#[inline]
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(
self,
Self::Timeout { .. } | Self::ConnectionError(_) | Self::DnsError(_)
)
}
#[inline]
#[must_use]
pub const fn is_client_error(&self) -> bool {
matches!(
self,
Self::InvalidUrl(_) | Self::InvalidRequest(_) | Self::SsrfBlocked(_)
)
}
#[inline]
#[must_use]
pub const fn is_tls_error(&self) -> bool {
matches!(self, Self::TlsError(_))
}
#[inline]
#[must_use]
pub const fn is_ssrf_blocked(&self) -> bool {
matches!(self, Self::SsrfBlocked(_))
}
#[inline]
#[must_use]
pub const fn timeout_ms(&self) -> Option<u64> {
match self {
Self::Timeout { timeout_ms } => *timeout_ms,
_ => None,
}
}
#[inline]
#[must_use]
pub fn message(&self) -> Option<&str> {
match self {
Self::DnsError(msg)
| Self::ConnectionError(msg)
| Self::TlsError(msg)
| Self::InvalidUrl(msg)
| Self::InvalidRequest(msg)
| Self::ResponseError(msg)
| Self::SsrfBlocked(msg)
| Self::Other(msg) => Some(msg),
Self::Timeout { .. } => None,
}
}
}
static DNS_PATTERNS: &[&[u8]] = &[
b"dns",
b"nxdomain",
b"no such host",
b"name resolution",
b"resolve",
b"getaddrinfo",
b"could not resolve",
];
static TIMEOUT_PATTERNS: &[&[u8]] = &[b"timeout", b"timed out", b"deadline exceeded", b"etimedout"];
static TLS_PATTERNS: &[&[u8]] = &[
b"certificate",
b"ssl",
b"tls",
b"handshake failed",
b"handshake error",
];
static CONNECTION_PATTERNS: &[&[u8]] = &[
b"connection refused",
b"econnrefused",
b"connection reset",
b"econnreset",
b"network unreachable",
b"enetunreach",
b"host unreachable",
b"ehostunreach",
b"connection failed",
b"failed to connect",
b"socket error",
b"i/o error",
b"io error",
b"connect error",
];
static REQUEST_PATTERNS: &[&[u8]] = &[
b"invalid header",
b"bad header",
b"invalid method",
b"unsupported method",
b"request too large",
b"body too large",
];
static RESPONSE_PATTERNS: &[&[u8]] = &[
b"response error",
b"body error",
b"stream error",
b"read error",
b"payload too large",
b"content too large",
];
#[inline]
fn contains_ci(haystack: &[u8], needle: &[u8]) -> bool {
if needle.len() > haystack.len() {
return false;
}
haystack
.windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle))
}
#[inline]
fn matches_any(error_bytes: &[u8], patterns: &[&[u8]]) -> bool {
patterns.iter().any(|p| contains_ci(error_bytes, p))
}
#[must_use]
pub fn map_wasi_error(wasi_error: &str) -> Error {
let error_bytes = wasi_error.as_bytes();
if matches_any(error_bytes, DNS_PATTERNS) {
return Error::DnsError(wasi_error.to_string());
}
if matches_any(error_bytes, TIMEOUT_PATTERNS) {
return Error::Timeout { timeout_ms: None };
}
if matches_any(error_bytes, TLS_PATTERNS)
|| contains_ci(error_bytes, b"cert ")
|| error_bytes.len() >= 4
&& error_bytes[error_bytes.len() - 4..].eq_ignore_ascii_case(b"cert")
{
return Error::TlsError(wasi_error.to_string());
}
if matches_any(error_bytes, CONNECTION_PATTERNS)
|| (contains_ci(error_bytes, b"connection") && !contains_ci(error_bytes, b"response"))
{
return Error::ConnectionError(wasi_error.to_string());
}
if matches_any(error_bytes, REQUEST_PATTERNS) {
return Error::InvalidRequest(wasi_error.to_string());
}
if matches_any(error_bytes, RESPONSE_PATTERNS)
|| (contains_ci(error_bytes, b"response") && contains_ci(error_bytes, b"failed"))
{
return Error::ResponseError(wasi_error.to_string());
}
Error::Other(wasi_error.to_string())
}
pub type Result<T> = std::result::Result<T, Error>;