Skip to main content

rtsp_runtime/
error.rs

1//! Error types for the RTSP session engine.
2//!
3//! Structured [`thiserror`] errors covering the failure surface of the sans-IO
4//! engine: RTSP message parse/serialize, state-machine violations (RFC 2326
5//! Appendix A — see `private/specs-verified/rtp/rtsp-runtime/state-machines.md`),
6//! CSeq correlation, missing/invalid `Session`, `Transport` header parsing
7//! (§12.39 — `private/specs-verified/rtp/rtsp-runtime/transport-header.md`),
8//! interleaved framing (§10.12 — `private/specs-verified/rtp/rtsp-runtime/interleaved-framing.md`),
9//! and authentication (§14 — `private/specs-verified/rtp/rtsp-runtime/auth.md`).
10
11use crate::Method;
12use crate::state::SessionState;
13
14/// Result alias for the engine's fallible operations.
15pub type Result<T> = core::result::Result<T, Error>;
16
17/// Errors produced by the RTSP session engine.
18#[non_exhaustive]
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21    /// An RTSP message could not be parsed or framed; `source` carries the
22    /// codec's or the framing check's own error.
23    #[error("failed to parse RTSP {what}")]
24    MessageParse {
25        /// What was being parsed.
26        what: MessageKind,
27        /// The underlying parse or framing error.
28        #[source]
29        source: Box<dyn std::error::Error + Send + Sync>,
30    },
31
32    /// The RTSP message could not be serialized by the underlying codec.
33    #[error("failed to serialize RTSP message")]
34    MessageWrite(#[source] rtsp_types::WriteError),
35
36    /// A method was invoked (client) or received (server) that is not permitted
37    /// in the current session state. The server maps this to `455 Method Not
38    /// Valid In This State` (RFC 2326 §11.3.6, Appendix A).
39    #[error("method {method:?} not valid in state {state:?}")]
40    MethodNotValidInState {
41        /// The offending method.
42        method: Method,
43        /// The state the object was in when the method was attempted.
44        state: SessionState,
45    },
46
47    /// A response arrived with no parseable `CSeq` header.
48    #[error("response is missing a CSeq header")]
49    MissingCSeq,
50
51    /// The `Transport` header value could not be parsed per RFC 2326 §12.39.
52    #[error("invalid Transport header: {0}")]
53    TransportParse(String),
54
55    /// A header value could not be serialized safely (control character in a value,
56    /// or a name that is not an RFC 2326 §15.1 token / session-id character).
57    #[error("cannot serialize header: {0}")]
58    HeaderSerialize(String),
59
60    /// The `Session` header value could not be parsed per RFC 2326 §12.37.
61    #[error("invalid Session header: {0}")]
62    SessionParse(String),
63
64    /// An interleaved `$` frame was malformed (RFC 2326 §10.12).
65    #[error("invalid interleaved frame: {0}")]
66    InterleavedFrame(String),
67
68    /// Authentication failed: no credentials were configured, the challenge
69    /// could not be parsed, or the digest computation failed (RFC 2326 §14).
70    #[error("authentication failure")]
71    Auth(#[from] broadcast_auth::Error),
72
73    /// The server answered `401 Unauthorized` without a `WWW-Authenticate`
74    /// challenge, so there is nothing to answer (RFC 2326 §14).
75    #[error("authentication failure: 401 without WWW-Authenticate")]
76    MissingAuthChallenge,
77
78    /// A socket IO operation failed in the async adapter (feature `tokio`).
79    #[error("socket IO error during {context}")]
80    Io {
81        /// The operation that failed.
82        context: IoContext,
83        /// The underlying socket error.
84        #[source]
85        source: std::io::Error,
86    },
87
88    /// A bounded wait in the async adapter expired (feature `tokio`).
89    #[error("timed out waiting for {what}")]
90    Timeout {
91        /// Which bounded wait expired: `"connect"`, `"handshake"`, `"read"` or `"write"`.
92        what: &'static str,
93    },
94
95    /// The server name for an `rtsps://` connection is not a valid TLS server
96    /// name (feature `tls`).
97    #[error("TLS error: invalid server name {server_name:?}")]
98    Tls {
99        /// The server name that was rejected.
100        server_name: String,
101        /// The underlying rustls error.
102        #[source]
103        source: Box<dyn std::error::Error + Send + Sync>,
104    },
105
106    /// The server answered `454 Session Not Found` (RFC 2326 §11.3.5) to a
107    /// request on our session — the session is gone (timed out, or torn down
108    /// server-side), so the caller must re-`SETUP`. Surfaced by the send-only
109    /// push drain (a `GET_PARAMETER` keepalive is the usual trigger) instead
110    /// of being silently discarded.
111    #[error("RTSP session not found (454 {method:?}): the session is gone; re-SETUP")]
112    SessionNotFound {
113        /// The method whose request the server rejected with 454.
114        method: Method,
115    },
116}
117
118/// What an [`Error::Io`] was doing when the socket operation failed.
119#[non_exhaustive]
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum IoContext {
122    /// Dialling the peer.
123    Connect,
124    /// The TLS handshake over an established connection.
125    TlsHandshake,
126    /// The peer closed the connection while a response or message was awaited.
127    Read,
128    /// A read or write failure surfaced by the framed codec.
129    Socket,
130}
131
132impl IoContext {
133    /// The label shown in [`Error::Io`]'s message.
134    pub fn name(&self) -> &'static str {
135        match self {
136            Self::Connect => "connect",
137            Self::TlsHandshake => "TLS handshake",
138            Self::Read => "read",
139            Self::Socket => "socket",
140        }
141    }
142}
143
144impl core::fmt::Display for IoContext {
145    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
146        f.write_str(self.name())
147    }
148}
149
150/// What an [`Error::MessageParse`] was parsing.
151#[non_exhaustive]
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum MessageKind {
154    /// An inbound request (server side).
155    Request,
156    /// An inbound response (client side).
157    Response,
158    /// A message whose direction is not yet known: the shared header framing check.
159    Framing,
160}
161
162impl MessageKind {
163    /// The label shown in [`Error::MessageParse`]'s message.
164    pub fn name(&self) -> &'static str {
165        match self {
166            Self::Request => "request",
167            Self::Response => "response",
168            Self::Framing => "message framing",
169        }
170    }
171}
172
173impl core::fmt::Display for MessageKind {
174    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
175        f.write_str(self.name())
176    }
177}
178
179impl From<std::io::Error> for Error {
180    fn from(e: std::io::Error) -> Self {
181        Error::Io {
182            context: IoContext::Socket,
183            source: e,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use std::error::Error as _;
192
193    #[test]
194    fn io_keeps_its_source_and_kind() {
195        let err = Error::from(std::io::Error::from(std::io::ErrorKind::ConnectionReset));
196        let src = err
197            .source()
198            .and_then(|s| s.downcast_ref::<std::io::Error>());
199        assert_eq!(
200            src.map(std::io::Error::kind),
201            Some(std::io::ErrorKind::ConnectionReset)
202        );
203    }
204
205    #[test]
206    fn message_parse_keeps_the_codec_error() {
207        let err = Error::MessageParse {
208            what: MessageKind::Response,
209            source: Box::new(rtsp_types::ParseError::Error),
210        };
211        assert!(
212            err.source()
213                .is_some_and(|s| s.downcast_ref::<rtsp_types::ParseError>().is_some())
214        );
215    }
216
217    #[test]
218    fn message_write_keeps_the_codec_error() {
219        let io = std::io::Error::from(std::io::ErrorKind::BrokenPipe);
220        let err = Error::MessageWrite(rtsp_types::WriteError::IoError(io));
221        assert!(
222            err.source()
223                .is_some_and(|s| s.downcast_ref::<rtsp_types::WriteError>().is_some())
224        );
225    }
226
227    fn chain(err: &Error) -> Vec<String> {
228        let mut out = vec![err.to_string()];
229        let mut cur = err.source();
230        while let Some(e) = cur {
231            out.push(e.to_string());
232            cur = e.source();
233        }
234        out
235    }
236
237    #[test]
238    fn no_message_in_a_source_chain_repeats_an_inner_one() {
239        let io = || std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe gone");
240        let errors = [
241            Error::MessageParse {
242                what: MessageKind::Request,
243                source: Box::new(rtsp_types::ParseError::Error),
244            },
245            Error::MessageWrite(rtsp_types::WriteError::IoError(io())),
246            Error::Io {
247                context: IoContext::Connect,
248                source: io(),
249            },
250            Error::from(io()),
251            Error::Tls {
252                server_name: "bad name".into(),
253                source: Box::new(io()),
254            },
255            Error::Auth(broadcast_auth::Error::ChallengeParse("bad".into())),
256        ];
257        for err in &errors {
258            let msgs = chain(err);
259            let (own, inner) = msgs.split_first().unwrap();
260            for message in inner {
261                assert!(
262                    !own.contains(message.as_str()),
263                    "{own:?} repeats {message:?}"
264                );
265            }
266        }
267    }
268
269    #[test]
270    fn contexts_name_what_was_being_done() {
271        let tls = Error::Tls {
272            server_name: "bad name".into(),
273            source: Box::new(std::io::Error::other("x")),
274        };
275        assert!(tls.to_string().contains("bad name"));
276        let parse = Error::MessageParse {
277            what: MessageKind::Request,
278            source: Box::new(rtsp_types::ParseError::Error),
279        };
280        assert!(parse.to_string().contains("request"));
281        assert_eq!(IoContext::TlsHandshake.to_string(), "TLS handshake");
282    }
283}