1use std::{fmt, io, path::PathBuf, time::Duration};
2
3
4#[derive(Debug, thiserror::Error)]
5pub enum StreamError {
6 #[error("IO error during {operation}: {source}")]
7 Io {
8 operation: String,
9 #[source]
10 source: io::Error,
11 },
12
13 #[error("File {operation} error for '{path}': {source}")]
14 File {
15 path: PathBuf,
16 operation: FileOperation,
17 #[source]
18 source: io::Error,
19 },
20
21 #[error("Network {operation} error for '{address}': {source}")]
22 Network {
23 address: String,
24 operation: NetworkOperation,
25 #[source]
26 source: io::Error,
27 },
28
29 #[error("Process {operation} error for '{command}': {source}")]
30 Process {
31 command: String,
32 operation: ProcessOperation,
33 #[source]
34 source: io::Error,
35 },
36
37 #[error("Stream {operation} error: {source}")]
38 Stream {
39 operation: StreamOperation,
40 #[source]
41 source: io::Error,
42 },
43
44 #[error("Codec '{codec_name}' error{}: {source}", position_display(.position))]
45 Codec {
46 codec_name: String,
47 position: Option<u64>,
48 #[source]
49 source: Box<dyn std::error::Error + Send + Sync>,
50 },
51
52 #[error("{limit_type} limit exceeded: {actual} > {max_allowed}")]
53 Limit {
54 limit_type: LimitType,
55 actual: u64,
56 max_allowed: u64,
57 },
58
59 #[error("Timeout during '{operation}' after {duration:?}")]
60 Timeout {
61 operation: String,
62 duration: Duration,
63 },
64
65 #[error("HTTP error: {source}")]
66 Http {
67 #[source]
68 source: reqwest::Error,
69 },
70
71 #[error("Content type detection failed{}{}: {reason}",
72 path_display(.path),
73 extension_display(.extension)
74 )]
75 ContentType {
76 path: Option<PathBuf>,
77 extension: Option<String>,
78 reason: String,
79 },
80
81 #[error("Failed to convert from '{from_type}' to '{to_type}': {reason}")]
82 Conversion {
83 from_type: String,
84 to_type: String,
85 reason: String,
86 },
87
88 #[error("{message}")]
89 Custom {
90 message: String,
91 #[source]
92 source: Option<Box<dyn std::error::Error + Send + Sync>>,
93 },
94
95 #[error("JSON error: {source}")]
96 Json {
97 #[source]
98 source: serde_json::Error,
99 },
100
101 #[error("URL parsing error: {source}")]
102 Url {
103 #[source]
104 source: url::ParseError,
105 },
106
107 #[error("UTF-8 conversion error: {source}")]
108 Utf8 {
109 #[source]
110 source: std::str::Utf8Error,
111 },
112
113 #[error("Stream ended unexpectedly")]
114 EndOfStream,
115
116 #[error("Stream operation was cancelled")]
117 Cancelled,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum FileOperation {
122 Open,
123 Read,
124 Write,
125 Seek,
126 Metadata,
127 Create,
128 Delete,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum NetworkOperation {
133 Connect,
134 Read,
135 Write,
136 Bind,
137 Listen,
138 Accept,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum ProcessOperation {
143 Spawn,
144 Wait,
145 Kill,
146 ReadStdout,
147 ReadStderr,
148 WriteStdin,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum StreamOperation {
153 Read,
154 Write,
155 Transform,
156 Buffer,
157 Flush,
158 Close,
159 Yield,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum LimitType {
164 FileSize,
165 StreamLength,
166 BufferSize,
167 ChunkSize,
168 Duration,
169 Bandwidth,
170}
171
172
173impl fmt::Display for FileOperation {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 write!(f, "{}", self.as_str())
176 }
177}
178
179impl fmt::Display for NetworkOperation {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 write!(f, "{}", self.as_str())
182 }
183}
184
185impl fmt::Display for ProcessOperation {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(f, "{}", self.as_str())
188 }
189}
190
191impl fmt::Display for StreamOperation {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 write!(f, "{}", self.as_str())
194 }
195}
196
197impl fmt::Display for LimitType {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "{}", self.as_str())
200 }
201}
202
203impl FileOperation {
204 pub const fn as_str(&self) -> &'static str {
205 match self {
206 FileOperation::Open => "open",
207 FileOperation::Read => "read",
208 FileOperation::Write => "write",
209 FileOperation::Seek => "seek",
210 FileOperation::Metadata => "metadata",
211 FileOperation::Create => "create",
212 FileOperation::Delete => "delete",
213 }
214 }
215}
216
217impl NetworkOperation {
218 pub const fn as_str(&self) -> &'static str {
219 match self {
220 NetworkOperation::Connect => "connect",
221 NetworkOperation::Read => "read",
222 NetworkOperation::Write => "write",
223 NetworkOperation::Bind => "bind",
224 NetworkOperation::Listen => "listen",
225 NetworkOperation::Accept => "accept",
226 }
227 }
228}
229
230impl ProcessOperation {
231 pub const fn as_str(&self) -> &'static str {
232 match self {
233 ProcessOperation::Spawn => "spawn",
234 ProcessOperation::Wait => "wait",
235 ProcessOperation::Kill => "kill",
236 ProcessOperation::ReadStdout => "read_stdout",
237 ProcessOperation::ReadStderr => "read_stderr",
238 ProcessOperation::WriteStdin => "write_stdin",
239 }
240 }
241}
242
243impl StreamOperation {
244 pub const fn as_str(&self) -> &'static str {
245 match self {
246 StreamOperation::Read => "read",
247 StreamOperation::Write => "write",
248 StreamOperation::Transform => "transform",
249 StreamOperation::Buffer => "buffer",
250 StreamOperation::Flush => "flush",
251 StreamOperation::Close => "close",
252 StreamOperation::Yield => "yield",
253 }
254 }
255}
256
257impl LimitType {
258 pub const fn as_str(&self) -> &'static str {
259 match self {
260 LimitType::FileSize => "file size",
261 LimitType::StreamLength => "stream length",
262 LimitType::BufferSize => "buffer size",
263 LimitType::ChunkSize => "chunk size",
264 LimitType::Duration => "duration",
265 LimitType::Bandwidth => "bandwidth",
266 }
267 }
268}
269
270
271fn position_display(position: &Option<u64>) -> String {
272 match position {
273 Some(pos) => format!(" at position {pos}" ),
274 None => String::new(),
275 }
276}
277
278fn path_display(path: &Option<PathBuf>) -> String {
279 match path {
280 Some(p) => format!(" for '{}'", p.display()),
281 None => String::new(),
282 }
283}
284
285fn extension_display(extension: &Option<String>) -> String {
286 match extension {
287 Some(ext) => format!(" (extension: {ext})" ),
288 None => String::new(),
289 }
290}
291
292impl From<std::io::Error> for StreamError {
293 fn from(err: std::io::Error) -> Self {
294 StreamError::Io {
295 operation: "unknown".to_string(),
296 source: err,
297 }
298 }
299}
300
301impl From<reqwest::Error> for StreamError {
302 fn from(err: reqwest::Error) -> Self {
303 StreamError::Http { source: err }
304 }
305}
306
307impl From<serde_json::Error> for StreamError {
308 fn from(err: serde_json::Error) -> Self {
309 StreamError::Json { source: err }
310 }
311}
312
313impl From<url::ParseError> for StreamError {
314 fn from(err: url::ParseError) -> Self {
315 StreamError::Url { source: err }
316 }
317}
318
319impl From<std::str::Utf8Error> for StreamError {
320 fn from(err: std::str::Utf8Error) -> Self {
321 StreamError::Utf8 { source: err }
322 }
323}
324
325
326impl StreamError {
327 pub fn io_error<O: Into<String>>(operation: O, source: std::io::Error) -> Self {
328 Self::Io {
329 operation: operation.into(),
330 source,
331 }
332 }
333
334 pub fn file_error<P: Into<PathBuf>>(
335 path: P,
336 operation: FileOperation,
337 source: std::io::Error,
338 ) -> Self {
339 Self::File {
340 path: path.into(),
341 operation,
342 source,
343 }
344 }
345
346 pub fn network_error<A: Into<String> >(
347 address: A,
348 operation: NetworkOperation,
349 source: io::Error,
350 ) -> Self {
351 Self::Network {
352 address: address.into(),
353 operation,
354 source ,
355 }
356 }
357
358 pub fn timeout_error<O: Into<String>>(operation: O, duration: Duration) -> Self {
359 Self::Timeout {
360 operation: operation.into(),
361 duration,
362 }
363 }
364
365 pub fn custom<M: Into<String>>(message: M) -> Self {
366 Self::Custom {
367 message: message.into(),
368 source: None,
369 }
370 }
371
372 pub fn size_limit_error(limit_type: LimitType, actual: u64, max_allowed: u64) -> Self {
373 Self::Limit {
374 limit_type,
375 actual,
376 max_allowed,
377 }
378 }
379
380 pub fn is_recoverable(&self) -> bool {
381 match self {
382 StreamError::Timeout { .. } => true,
383 StreamError::Network { .. } => true,
384 StreamError::Http { source } => source.is_timeout() || source.is_connect(),
385 StreamError::Io { source, .. } => {
386 matches!(
387 source.kind(),
388 std::io::ErrorKind::Interrupted
389 | std::io::ErrorKind::WouldBlock
390 | std::io::ErrorKind::TimedOut
391 )
392 }
393 StreamError::Limit { .. } => false,
394 StreamError::Cancelled => true,
395 _ => false,
396 }
397 }
398
399 pub fn is_temporary(&self) -> bool {
400 match self {
401 StreamError::Timeout { .. } => true,
402 StreamError::Network { .. } => true,
403 StreamError::Http { source } => {
404 source.is_timeout()
405 || source.is_connect()
406 || source.status().is_some_and( |s| s.is_server_error())
407 }
408 StreamError::Io { source, .. } => {
409 matches!(
410 source.kind(),
411 std::io::ErrorKind::Interrupted
412 | std::io::ErrorKind::WouldBlock
413 | std::io::ErrorKind::TimedOut
414 | std::io::ErrorKind::ConnectionRefused
415 | std::io::ErrorKind::ConnectionReset
416 )
417 }
418 StreamError::Cancelled => true,
419 _ => false,
420 }
421 }
422}
423
424#[derive(Debug, thiserror::Error)]
425pub enum HeaderError {
426 #[error("Invalid header name '{name}': {reason}")]
427 InvalidHeaderName { name: String, reason: String },
428
429 #[error("Invalid header value for '{name}' = '{value}': {reason}")]
430 InvalidHeaderValue {
431 name: String,
432 value: String,
433 reason: String
434 },
435
436 #[error("Encoding failed for header '{name}': {reason}")]
437 EncodingFailed { name: String, reason: String },
438 #[error("Header value contains invalid UTF-8: {reason}")]
439 InvalidUtf8 {
440 reason: String
441 },
442}