Skip to main content

audio_engine_core/decoder/
error.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3#[cfg(feature = "http")]
4use std::time::Duration;
5
6use thiserror::Error;
7
8#[cfg(feature = "http")]
9const NETWORK_MAX_ATTEMPTS: usize = 3;
10#[cfg(feature = "http")]
11const NETWORK_BACKOFF_DELAYS: [Duration; 2] = [Duration::from_secs(1), Duration::from_secs(2)];
12
13/// Classification of network failures encountered while decoding from an
14/// HTTP(S) source.
15///
16/// Only available with the `http` feature.
17#[cfg(feature = "http")]
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum NetworkError {
20    /// The request exceeded its configured timeout.
21    HttpTimeout,
22    /// The connection was reset by the peer mid-transfer.
23    ConnectionReset,
24    /// The server returned a non-success status code.
25    HttpStatus(u16),
26    /// DNS resolution failed for the request host.
27    DnsFailure(String),
28    /// The TLS handshake or certificate validation failed.
29    TlsError(String),
30    /// Any other transport error, carrying its description.
31    Other(String),
32}
33
34#[cfg(feature = "http")]
35impl NetworkError {
36    /// Returns `true` when retrying the operation may succeed (transient
37    /// timeouts, connection resets, and 408/429/5xx statuses).
38    pub fn is_retriable(&self) -> bool {
39        match self {
40            NetworkError::HttpTimeout | NetworkError::ConnectionReset => true,
41            NetworkError::HttpStatus(status) => matches!(status, 408 | 429 | 500..=504),
42            NetworkError::DnsFailure(_) | NetworkError::TlsError(_) | NetworkError::Other(_) => {
43                false
44            }
45        }
46    }
47
48    pub(super) fn from_io(e: std::io::Error) -> Self {
49        match e.kind() {
50            std::io::ErrorKind::TimedOut => NetworkError::HttpTimeout,
51            std::io::ErrorKind::ConnectionReset => NetworkError::ConnectionReset,
52            _ => NetworkError::Other(e.to_string()),
53        }
54    }
55
56    fn is_decode_cancelled(&self) -> bool {
57        matches!(self, NetworkError::Other(message) if message == "Decode cancelled")
58    }
59}
60
61#[cfg(feature = "http")]
62pub(super) fn network_error_to_decoder_error(error: NetworkError) -> DecoderError {
63    if error.is_decode_cancelled() {
64        DecoderError::Canceled
65    } else {
66        DecoderError::Network(error)
67    }
68}
69
70#[cfg(feature = "http")]
71impl From<reqwest::Error> for NetworkError {
72    fn from(e: reqwest::Error) -> Self {
73        if e.is_timeout() {
74            NetworkError::HttpTimeout
75        } else if let Some(status) = e.status() {
76            NetworkError::HttpStatus(status.as_u16())
77        } else {
78            let text = e.to_string();
79            let lower = text.to_ascii_lowercase();
80            if lower.contains("connection reset") {
81                NetworkError::ConnectionReset
82            } else if e.is_connect() && (lower.contains("dns") || lower.contains("resolve")) {
83                NetworkError::DnsFailure(text)
84            } else if lower.contains("tls") || lower.contains("certificate") {
85                NetworkError::TlsError(text)
86            } else {
87                NetworkError::Other(text)
88            }
89        }
90    }
91}
92
93#[cfg(feature = "http")]
94impl std::fmt::Display for NetworkError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            NetworkError::HttpTimeout => write!(f, "HTTP timeout"),
98            NetworkError::ConnectionReset => write!(f, "connection reset"),
99            NetworkError::HttpStatus(status) => write!(f, "HTTP status {}", status),
100            NetworkError::DnsFailure(e) => write!(f, "DNS failure: {}", e),
101            NetworkError::TlsError(e) => write!(f, "TLS error: {}", e),
102            NetworkError::Other(e) => write!(f, "{}", e),
103        }
104    }
105}
106
107/// Cooperative cancellation handle shared with a running decode operation.
108#[derive(Clone, Debug)]
109pub struct DecodeCancelToken {
110    cancelled: Arc<AtomicBool>,
111}
112
113impl DecodeCancelToken {
114    /// Wrap a shared cancellation flag for use with the decoder open/decode
115    /// entry points.
116    pub fn new(cancelled: Arc<AtomicBool>) -> Self {
117        Self { cancelled }
118    }
119
120    /// Returns `true` once the underlying flag has been set, signalling that
121    /// the decode should stop as soon as possible.
122    pub fn is_cancelled(&self) -> bool {
123        self.cancelled.load(Ordering::Acquire)
124    }
125}
126
127/// Errors returned by the streaming decoder.
128#[derive(Error, Debug)]
129pub enum DecoderError {
130    /// A local file could not be opened.
131    #[error("Failed to open file: {0}")]
132    FileOpen(#[from] std::io::Error),
133    /// An HTTP(S) source failed. Only constructible with the `http` feature.
134    #[cfg(feature = "http")]
135    #[error("Network error: {0}")]
136    Network(NetworkError),
137    /// The container format is not supported.
138    #[error("Unsupported format")]
139    UnsupportedFormat,
140    /// No decodable audio track was found in the container.
141    #[error("No audio track found")]
142    NoAudioTrack,
143    /// The codec failed to decode a packet.
144    #[error("Decoder error: {0}")]
145    Decoder(String),
146    /// Format probing failed.
147    #[error("Probe error: {0}")]
148    Probe(String),
149    /// The decode was cancelled via a [`DecodeCancelToken`].
150    #[error("Decode cancelled")]
151    Canceled,
152}
153
154#[cfg(feature = "http")]
155// `attempt` indexes NETWORK_BACKOFF_DELAYS, a different array than the loop range.
156#[allow(clippy::needless_range_loop)]
157pub(super) fn with_network_retry<T, F>(operation_name: &str, mut op: F) -> Result<T, NetworkError>
158where
159    F: FnMut() -> Result<T, NetworkError>,
160{
161    for attempt in 0..NETWORK_MAX_ATTEMPTS {
162        match op() {
163            Ok(value) => return Ok(value),
164            Err(e) if e.is_retriable() && attempt < NETWORK_BACKOFF_DELAYS.len() => {
165                let delay = NETWORK_BACKOFF_DELAYS[attempt];
166                log::warn!(
167                    "{} attempt {} failed ({}), retrying in {:?}",
168                    operation_name,
169                    attempt + 1,
170                    e,
171                    delay
172                );
173                std::thread::sleep(delay);
174            }
175            Err(e) => return Err(e),
176        }
177    }
178
179    unreachable!("network retry loop returns on success or final error")
180}