Skip to main content

sipx_call/
error.rs

1//! Call errors.
2
3use sipx_sip::{HeaderName, Method};
4use thiserror::Error;
5
6/// What can go wrong establishing or running a call.
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum Error {
10    /// A socket failed.
11    #[error("io: {0}")]
12    Io(#[source] std::io::Error),
13    /// Negotiated media could not be constructed safely.
14    #[error("media: {0}")]
15    Media(#[from] sipx_media::SetupError),
16    /// The transport failed.
17    #[error("transport: {0}")]
18    Transport(#[from] sipx_transport::Error),
19    /// A message could not be built.
20    #[error("build: {0}")]
21    Build(#[from] sipx_sip::error::BuildError),
22    /// A selected authentication service could not attest the outbound caller identity.
23    #[error("caller identity: {0}")]
24    IdentityAuthentication(#[from] sipx_ua::identity::AuthenticationError),
25    /// The SDP could not be read.
26    #[error("sdp: {0}")]
27    Sdp(String),
28    /// A named browser-audio policy boundary refused setup or renegotiation.
29    #[error("browser-audio profile: {0}")]
30    Profile(#[from] sipx_sdp::browser_audio::ProfileError),
31    /// SDP was asked to advertise an unspecified address which no peer can reach.
32    #[error("the advertised media address must not be unspecified")]
33    UnspecifiedMediaAddress,
34    /// DTLS-SRTP was selected in a build that does not contain its handshake implementation.
35    #[error("DTLS-SRTP was selected, but sipx-call was built without its `dtls` feature")]
36    DtlsUnavailable,
37    /// A selected DTLS-SRTP media path could not be keyed.
38    #[error("DTLS-SRTP: {0}")]
39    Dtls(String),
40    /// The DTLS setup exchange selected no role this endpoint can hold.
41    #[error("DTLS setup: {0}")]
42    DtlsSetup(#[from] sipx_sdp::fingerprint::SetupRoleError),
43    /// DTLS-SRTP was selected through an early-dialog API that cannot yet preserve its ordering.
44    #[error("DTLS-SRTP is not available for early media")]
45    DtlsEarlyMedia,
46    /// A running DTLS-SRTP call was asked to renegotiate without a rekeying exchange.
47    #[error("DTLS-SRTP renegotiation is not implemented; the existing call is unchanged")]
48    DtlsRenegotiation,
49    /// An in-dialog description tried to change which sockets carry RTCP.
50    #[error(
51        "RTCP mode change from {current:?} to {proposed:?} is not implemented; the existing call is unchanged"
52    )]
53    RtcpModeChange {
54        /// The mode owned by the running media session.
55        current: sipx_sdp::RtcpMode,
56        /// The mode the new offer/answer exchange selected.
57        proposed: sipx_sdp::RtcpMode,
58    },
59    /// The INVITE got no final response.
60    #[error("no final response to the INVITE")]
61    NoResponse,
62    /// The far end refused.
63    #[error("rejected: {status} {reason}")]
64    Rejected {
65        /// The status code.
66        status: u16,
67        /// Its reason phrase.
68        reason: String,
69    },
70    /// A supported digest challenge returned by an INVITE attempt.
71    ///
72    /// [`crate::dial`] and [`crate::dial_once`] consume this internally when credentials were
73    /// supplied. It can surface from [`crate::dial_early`], whose handle names one INVITE and
74    /// therefore does not silently replace it with a retry.
75    #[error("authentication required: {status} {reason}")]
76    AuthenticationChallenge {
77        /// The 401 or 407 status.
78        status: u16,
79        /// Its reason phrase.
80        reason: String,
81        /// The strongest supported challenge the response offered.
82        challenge: Box<sipx_sip::auth::Challenge>,
83    },
84    /// A 2xx that established no dialog — no `To` tag, or no `Contact` to send to.
85    #[error("the response established no dialog")]
86    NoDialog,
87    /// The caller gave up before the far end answered, and cancelled the invitation.
88    ///
89    /// Distinct from a rejection: nobody refused the call, we stopped waiting for it.
90    #[error("no answer within {0:?}; the invitation was cancelled")]
91    Cancelled(std::time::Duration),
92    /// An invitation was answered after the caller had already withdrawn it (RFC 3261 §9.2).
93    ///
94    /// The other side of [`Self::Cancelled`]: there, *this* stack gave up on an INVITE it sent;
95    /// here, the far end gave up on one it sent us, the invitation was answered `487 Request
96    /// Terminated`, and there is nothing left to accept. Answering anyway would put a `200` on a
97    /// transaction that has already finished and leave this side holding a call the caller does
98    /// not have.
99    #[error("the invitation was cancelled by the caller")]
100    InvitationCancelled,
101    /// A caller-selected signalling-dialog tag was empty, overlong or not a SIP token.
102    ///
103    /// The rejected value is deliberately absent: reflecting arbitrary application or fixture
104    /// bytes in an error is unnecessary and makes it too easy to copy them into a log.
105    #[error("the signalling dialog tag must be a non-empty SIP token of at most 128 bytes")]
106    InvalidDialogTag,
107    /// An originated BYE did not receive a final response inside its caller-owned failure bound.
108    #[error("no final response to the dialog BYE within {0:?}")]
109    SignallingTeardownTimeout(std::time::Duration),
110    /// A final response to an originated BYE did not name that dialog and `CSeq`.
111    ///
112    /// The mismatching values are intentionally absent so an untrusted packet is not reflected
113    /// into logs by displaying this error.
114    #[error("the dialog BYE response did not match its dialog and CSeq")]
115    InvalidDialogResponse,
116    /// An INVITE asked to replace a dialog it did not name, or named one this is not.
117    ///
118    /// Deliberately one error for both. Telling a caller "the Call-ID matched but the tags did
119    /// not" would be telling them how far their guess got.
120    #[error("the Replaces header names no dialog we have")]
121    NoReplaces,
122    /// A transfer was accepted or refused when none had been asked for.
123    #[error("no transfer has been requested on this call")]
124    NoReferral,
125    /// Negotiation produced no usable audio stream.
126    #[error("no codec in common")]
127    NoCommonCodec,
128    /// A `422` refused the session interval we asked for (RFC 4028 §6), naming its own minimum.
129    ///
130    /// Carries the minimum rather than folding into [`Self::Rejected`] because the whole point
131    /// of a 422 is that it is retryable, and only the value it carries makes the retry possible.
132    #[error("the far end requires a session interval of at least {0:?}")]
133    IntervalTooBrief(std::time::Duration),
134    /// The far end never refreshed the session, so it was torn down locally (RFC 4028 §10).
135    #[error("the session expired without a refresh")]
136    SessionExpired,
137    /// A 2xx was asked for while a reliable provisional carrying SDP is unacknowledged.
138    ///
139    /// RFC 3262 §5 makes the delay a MUST, and this is where it is enforced rather than
140    /// silently deferred: a description sent in a provisional that never arrived, followed by a
141    /// 200 that carries none, leaves the caller in a confirmed dialog with no answer at all.
142    /// Keep feeding messages to [`Ringing::on_prack`](crate::Ringing::on_prack) and try again.
143    #[error("the reliable provisional carrying the answer has not been acknowledged")]
144    UnacknowledgedProvisional,
145    /// An invitation was treated as having an early session when it never had one.
146    ///
147    /// On the answering side: either it was rung with `ring` rather than `ring_early`, or it has
148    /// already been answered — a `Ringing` hands its media port and its dialog over exactly once.
149    ///
150    /// On the calling side, from [`Dialing::update`](crate::Dialing::update): the far end has
151    /// established a dialog but has not answered our offer in a reliable provisional, so RFC
152    /// 3311 §5.1 does not yet allow an UPDATE to carry one.
153    /// [`Dialing::has_early_session`](crate::Dialing::has_early_session) is the same question
154    /// asked in advance.
155    #[error("this invitation has no early session to answer")]
156    NoEarlySession,
157    /// A stack-owned or unadmitted method was passed to the application-owned dialog API.
158    #[error("{0} is not an application-owned method on this dialog")]
159    StackOwnedDialogMethod(Method),
160    /// Application content exceeded the retained request-body bound.
161    #[error("application-owned body is {actual} octets; the limit is {limit}")]
162    ApplicationBodyTooLarge {
163        /// Observed body length.
164        actual: usize,
165        /// Maximum retained body length.
166        limit: usize,
167    },
168    /// A non-empty application-owned body did not name its media type.
169    #[error("a non-empty application-owned body requires Content-Type")]
170    ApplicationContentTypeRequired,
171    /// An application tried to supply a dialog- or transaction-owned header.
172    #[error("the stack owns the {0:?} header on in-dialog requests")]
173    ProtectedApplicationHeader(HeaderName),
174    /// An application response must end the transaction rather than being provisional.
175    #[error("application response status {0} is provisional; a final response is required")]
176    ApplicationFinalResponseRequired(u16),
177    /// The request's exactly-once response capability was already claimed.
178    #[error("the application-owned request has already been answered")]
179    ApplicationResponseAlreadySent,
180    /// A response capability could not capture the runtime that owns its transport work.
181    #[error("an application response requires an active Tokio runtime")]
182    ApplicationRuntimeUnavailable,
183    /// An operation requiring a live dialog was attempted after call teardown.
184    #[error("the dialog has ended")]
185    DialogEnded,
186}
187
188/// A call result.
189pub type Result<T> = std::result::Result<T, Error>;