1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum SequenceKind {
6 Media,
8 Discontinuity,
10}
11
12impl std::fmt::Display for SequenceKind {
13 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14 match self {
15 Self::Media => write!(f, "media"),
16 Self::Discontinuity => write!(f, "discontinuity"),
17 }
18 }
19}
20
21pub(crate) fn status_retryable(status: u16) -> bool {
27 matches!(status, 408 | 429 | 502 | 503 | 504)
28}
29
30#[derive(Debug, Clone, thiserror::Error)]
32#[non_exhaustive]
33pub enum Error {
34 #[error("moq: {0}")]
36 Moq(#[from] moq_net::Error),
37
38 #[error("mux: {0}")]
40 Mux(#[from] moq_mux::Error),
41
42 #[error("resource is outside the playlist's origin")]
44 ResourceRefused,
45
46 #[error("invalid file URL")]
48 InvalidFileUrl,
49
50 #[error("failed to parse HLS playlist: {0}")]
52 ParsePlaylist(String),
53
54 #[error("no usable variants found in master playlist")]
56 NoVariants,
57
58 #[error("playlist missing EXT-X-MAP")]
60 MissingMap,
61
62 #[error("encountered segment with empty URI")]
64 EmptySegmentUri,
65
66 #[error("implicit byte range for {url} has no preceding range for the same resource")]
68 MissingByteRangeOffset {
69 url: url::Url,
71 },
72
73 #[error("invalid byte range {start}+{length} for {url}")]
75 InvalidByteRange {
76 url: url::Url,
78 start: u64,
80 length: u64,
82 },
83
84 #[error("byte range for {url} returned {actual} bytes, expected {expected}")]
86 ByteRangeLengthMismatch {
87 url: url::Url,
89 expected: u64,
91 actual: usize,
93 },
94
95 #[error("byte range for {url} returned a mismatched Content-Range, expected bytes {start}-{end}")]
97 ByteRangeResponseMismatch {
98 url: url::Url,
100 start: u64,
102 end: u64,
104 },
105
106 #[error("HLS {kind} sequence {value} is too large to encode")]
108 SequenceOverflow {
109 kind: SequenceKind,
111 value: u64,
113 },
114
115 #[error("invalid generation {0:?}: expected ASCII letters, digits, '-', or '_'")]
117 InvalidGeneration(String),
118
119 #[error("url parse: {0}")]
121 UrlParse(#[from] url::ParseError),
122
123 #[error("reqwest: {0}")]
125 Reqwest(std::sync::Arc<reqwest::Error>),
126
127 #[error("io: {0}")]
129 Io(std::sync::Arc<std::io::Error>),
130}
131
132impl Error {
133 pub fn status(&self) -> Option<u16> {
139 match self {
140 Self::Reqwest(err) => err.status().map(|status| status.as_u16()),
141 _ => None,
142 }
143 }
144}
145
146impl From<reqwest::Error> for Error {
147 fn from(err: reqwest::Error) -> Self {
148 Error::Reqwest(std::sync::Arc::new(err.without_url()))
151 }
152}
153
154impl From<std::io::Error> for Error {
155 fn from(err: std::io::Error) -> Self {
156 Error::Io(std::sync::Arc::new(err))
157 }
158}
159
160pub type Result<T> = std::result::Result<T, Error>;
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
169 fn an_http_failure_reports_its_status() {
170 assert_eq!(Error::NoVariants.status(), None);
172 assert_eq!(Error::ParsePlaylist("not a playlist".to_string()).status(), None);
173 assert_eq!(Error::Moq(moq_net::Error::Transport("lost".to_string())).status(), None);
174 }
175
176 #[tokio::test]
177 async fn http_error_redacts_url() {
178 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
180 let address = listener.local_addr().expect("local address");
181 drop(listener);
182
183 let err: Error = reqwest::get(format!("http://user:pass@{address}/media.m3u8?jwt=secret"))
184 .await
185 .expect_err("dial a closed port")
186 .into();
187
188 let printed = format!("{err} {err:?}");
189 for secret in ["jwt", "secret", "user:pass"] {
190 assert!(!printed.contains(secret), "error leaked {secret}: {printed}");
191 }
192 }
193}