Skip to main content

ez_ffmpeg/http_input/
error.rs

1//! Typed errors for the experimental `http-input` feature.
2//!
3//! Sensitive header values and URL userinfo / query / fragment must never
4//! appear in these messages.
5
6use std::fmt;
7
8/// Kind of multi-resource manifest that `HttpInput` refuses.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ManifestKind {
12    /// HLS (`application/vnd.apple.mpegurl`, `#EXTM3U`, `.m3u8`).
13    Hls,
14    /// MPEG-DASH (`application/dash+xml`, `<MPD`, `.mpd`).
15    Dash,
16}
17
18impl fmt::Display for ManifestKind {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Hls => f.write_str("HLS"),
22            Self::Dash => f.write_str("DASH"),
23        }
24    }
25}
26
27/// Failure from building or opening an [`crate::http_input::HttpInput`].
28///
29/// This type is boxed into [`crate::error::Error::HttpInput`] so the crate
30/// `Error` layout stays within 64 bytes.
31#[derive(Debug, Clone, thiserror::Error)]
32#[non_exhaustive]
33pub enum HttpInputError {
34    /// The URL is not a usable `http://` or `https://` resource.
35    #[error("invalid HTTP input URL: {reason}")]
36    InvalidUrl {
37        /// Stable English reason (no URL payload).
38        reason: &'static str,
39    },
40
41    /// Userinfo in the URL is rejected; use the Authorization header API.
42    #[error(
43        "HTTP input URLs must not contain userinfo; pass credentials with the Authorization header"
44    )]
45    UserinfoForbidden,
46
47    /// HLS / DASH (or another nested-resource format) is out of v1 scope.
48    #[error(
49        "Rust HTTP input supports one HTTP resource per input; {kind} manifests \
50         open nested resources and are not supported. Use Input::from(url) with an \
51         FFmpeg build that includes HTTPS/TLS, or resolve the manifest outside \
52         ez-ffmpeg and provide a single media stream"
53    )]
54    ManifestUnsupported {
55        /// Which manifest family was detected.
56        kind: ManifestKind,
57    },
58
59    /// The demuxer asked to open a second resource.
60    #[error(
61        "Rust HTTP input rejected a nested resource requested by the demuxer. This \
62         input format is not supported by the single-resource HttpInput API"
63    )]
64    NestedResourceUnsupported,
65
66    /// Server compressed the entity body; Range offsets would not match.
67    #[error("HTTP server returned a non-identity Content-Encoding; media input requires identity")]
68    UnsupportedContentEncoding,
69
70    /// HTTP status that does not have a more specific variant.
71    #[error("HTTP request failed with status {code}")]
72    Status {
73        /// Status code from the origin.
74        code: u16,
75    },
76
77    /// HTTP 401.
78    #[error("HTTP authentication required")]
79    AuthenticationRequired,
80
81    /// HTTP 403.
82    #[error("HTTP access denied")]
83    AccessDenied,
84
85    /// HTTP 407.
86    #[error("HTTP proxy authentication required")]
87    ProxyAuthenticationRequired,
88
89    /// HTTP 404 / 410.
90    #[error("HTTP resource not found")]
91    NotFound,
92
93    /// Connect / response-header deadline.
94    #[error("HTTP request timed out")]
95    Timeout,
96
97    /// No body bytes arrived within the idle window.
98    #[error("HTTP body idle timeout")]
99    ReadIdleTimeout,
100
101    /// rustls rejected the certificate or hostname.
102    #[error("TLS certificate or hostname verification failed")]
103    TlsVerification,
104
105    /// Range request could not be satisfied.
106    #[error("HTTP range is not satisfiable")]
107    RangeNotSatisfiable,
108
109    /// Non-zero Range was ignored (server returned 200 for a mid-resource seek).
110    #[error("The HTTP server does not support byte-range requests required to seek this input")]
111    RangeIgnored,
112
113    /// ETag / Last-Modified / size changed across Range or reconnect.
114    #[error("HTTP resource changed between requests")]
115    ResourceChanged,
116
117    /// Redirect loop or hop limit.
118    #[error("too many HTTP redirects")]
119    TooManyRedirects,
120
121    /// HTTPS followed by an `http://` Location.
122    #[error("refusing HTTPS to HTTP redirect")]
123    HttpsDowngrade,
124
125    /// Caller tried to set a hop-by-hop or transport header the crate owns.
126    #[error("HTTP header '{name}' is reserved by HttpInput")]
127    HeaderReserved {
128        /// Header name the caller attempted to set.
129        name: String,
130    },
131
132    /// Header name or value failed HTTP validation.
133    #[error("invalid HTTP header '{name}'")]
134    HeaderInvalid {
135        /// Header name that failed validation.
136        name: String,
137    },
138
139    /// System / custom trust store produced no usable anchors.
140    #[error("no usable TLS trust anchors; add a custom CA or install system roots")]
141    NoTrustAnchors,
142
143    /// PEM did not contain a certificate.
144    #[error("invalid TLS certificate PEM")]
145    InvalidCertificate,
146
147    /// PEM identity was missing a cert chain or an unencrypted private key.
148    #[error("invalid TLS client identity PEM (need cert chain and unencrypted private key)")]
149    IdentityInvalid,
150
151    /// Proxy URL or credentials were unusable.
152    #[error("invalid HTTP proxy configuration")]
153    InvalidProxy,
154
155    /// Timeout builder rejected a zero duration.
156    #[error("HTTP timeouts must be greater than zero")]
157    InvalidTimeout,
158
159    /// Transport / DNS / connection failure (message has no secrets).
160    #[error("HTTP transport error: {message}")]
161    Transport {
162        /// Redacted transport description.
163        message: String,
164    },
165
166    /// Scheduler stop / abort observed on the AVIO poll tick.
167    #[error("HTTP input interrupted")]
168    Interrupted,
169
170    /// Socket EOF arrived before the declared Content-Length or Content-Range total.
171    #[error("HTTP response body was truncated before the declared length")]
172    TruncatedBody,
173}
174
175impl HttpInputError {
176    pub(crate) fn to_errno(&self) -> i32 {
177        use ffmpeg_sys_next::{
178            AVERROR, AVERROR_EXIT, EACCES, ECONNRESET, EHOSTUNREACH, EINVAL, EIO, ELOOP, ENOENT,
179            ENOSYS, EPERM, ETIMEDOUT,
180        };
181        match self {
182            Self::Interrupted => AVERROR_EXIT,
183            Self::NotFound => AVERROR(ENOENT),
184            Self::Timeout | Self::ReadIdleTimeout => AVERROR(ETIMEDOUT),
185            Self::AuthenticationRequired
186            | Self::AccessDenied
187            | Self::ProxyAuthenticationRequired
188            | Self::TlsVerification => AVERROR(EACCES),
189            Self::NestedResourceUnsupported => AVERROR(EPERM),
190            Self::TooManyRedirects => AVERROR(ELOOP),
191            Self::InvalidUrl { .. }
192            | Self::UserinfoForbidden
193            | Self::HeaderReserved { .. }
194            | Self::HeaderInvalid { .. }
195            | Self::RangeNotSatisfiable
196            | Self::Status { code: 400 } => AVERROR(EINVAL),
197            Self::RangeIgnored => AVERROR(ENOSYS),
198            Self::Transport { message } if message.contains("reset") => AVERROR(ECONNRESET),
199            Self::Transport { message }
200                if message.contains("dns") || message.contains("resolve") =>
201            {
202                AVERROR(EHOSTUNREACH)
203            }
204            _ => AVERROR(EIO),
205        }
206    }
207}