Skip to main content

moqtap_proxy/
error.rs

1//! Proxy error types.
2
3use moqtap_client::transport::TransportError;
4use moqtap_codec::error::CodecError;
5use moqtap_codec::version::DraftVersion;
6
7use crate::transport::TransportProfileError;
8use crate::types::Leg;
9
10/// Errors from the proxy layer.
11#[derive(Debug, thiserror::Error)]
12pub enum ProxyError {
13    /// Error from the QUIC/WebTransport listener.
14    #[error("listener error: {0}")]
15    Listener(String),
16    /// Something was asked of a proxy's listener before there was one.
17    ///
18    /// A [`TransparentProxy`](crate::proxy::TransparentProxy) binds its
19    /// endpoint inside `run()`, so a
20    /// [`ProxyControl`](crate::control::ProxyControl) taken beforehand —
21    /// which is the usual case, since `run()` does not return until the
22    /// proxy is finished — is live before the endpoint is. This is the
23    /// answer during that window, and again after `run()` has returned and
24    /// the endpoint is gone.
25    ///
26    /// Distinct from [`ProxyError::Listener`] on purpose: that one means a
27    /// listener existed and something about it failed, and the two call for
28    /// opposite responses. A caller waiting for a proxy to come up retries
29    /// on this and gives up on the other.
30    #[error("the proxy has not bound a listener")]
31    NotBound,
32    /// Error from the underlying transport.
33    #[error("transport error: {0}")]
34    Transport(#[from] TransportError),
35    /// Error decoding a MoQT frame.
36    #[error("codec error: {0}")]
37    Codec(#[from] CodecError),
38    /// Failed to connect to the upstream relay.
39    #[error("upstream connection failed: {0}")]
40    UpstreamConnect(String),
41    /// A socket was supplied for an upstream connection that cannot be
42    /// made over it.
43    ///
44    /// Distinct from [`ProxyError::UpstreamConnect`] because nothing was
45    /// attempted: the session refuses to connect at all rather than
46    /// connecting over a socket the caller did not supply. Silently
47    /// ignoring the socket would let a caller arm loss, delay or a
48    /// bandwidth cap on the relay leg, watch a clean run, and conclude the
49    /// impairment had no effect — when in fact it never reached the wire.
50    #[error(
51        "upstream socket unsupported: a WebTransport upstream builds its endpoint inside the \
52         WebTransport library, which exposes no way to supply a userspace socket, so the socket \
53         supplied for this session cannot be honoured"
54    )]
55    UpstreamSocketUnsupported,
56    /// One leg was given both a raw `quinn::TransportConfig` and a
57    /// [`TransportProfile`](crate::transport::TransportProfile).
58    ///
59    /// Refused where the leg is built, rather than merged, and the reason
60    /// is a missing trait rather than a policy anyone chose.
61    /// `quinn::TransportConfig` has no `Clone` and no getter for any field,
62    /// so nothing in this crate can take the caller's config and hand back
63    /// a modified copy of it. Code that looked like a merge would in fact
64    /// be starting from `quinn::TransportConfig::default()` and discarding
65    /// everything the embedding program configured — and the tests would
66    /// stay green while it happened, because every field set *in the
67    /// profile* does arrive. Only the fields set in the config vanish, and
68    /// nothing reports that.
69    ///
70    /// A caller who wants both writes the base config and applies the
71    /// profile over it with `TransportProfile::apply_to(&mut base)`, then
72    /// sets the result as this leg's `transport_config`. Where the base has
73    /// to be rebuilt for each connection,
74    /// [`TransportInstaller`](crate::transport::TransportInstaller) is that
75    /// same call behind a trait the leg can hold.
76    #[error(
77        "the {leg:?} leg was given both a transport_config and a transport_profile, which cannot \
78         be combined: quinn::TransportConfig can be neither cloned nor read back, so a merge \
79         would silently discard the config and keep only the profile. Apply the profile to your \
80         own config with TransportProfile::apply_to(&mut base) and set the result as this leg's \
81         transport_config, or supply a TransportInstaller that builds the base itself"
82    )]
83    TransportConfigAndProfile {
84        /// The leg holding the contradiction.
85        leg: Leg,
86    },
87    /// One leg was given both a raw `quinn::TransportConfig` and a
88    /// [`QlogSpec`](crate::qlog::QlogSpec).
89    ///
90    /// Its own variant rather than a widening of
91    /// [`ProxyError::TransportConfigAndProfile`], because the two refusals
92    /// have different fixes and a caller — or a test — that could not tell
93    /// them apart would be sent to the wrong half of its configuration. A
94    /// single variant carrying a string to say which pair was named is not
95    /// something a `match` can check.
96    ///
97    /// # Why the pair cannot be honoured
98    ///
99    /// quinn accepts a capture sink in exactly one place: a method that
100    /// **mutates** a `quinn::TransportConfig`. There is no setter on an
101    /// endpoint, none on a `quinn::ServerConfig` or a `quinn::ClientConfig`,
102    /// and none on a live connection. A leg's raw config arrives as an
103    /// `Arc<quinn::TransportConfig>`, and that type has no `Clone`, so it
104    /// cannot be copied and mutated; `Arc::get_mut` is the only other way in
105    /// and it hands back nothing whenever a second handle exists — which it
106    /// does on this crate's own path, since
107    /// [`TransparentProxy`](crate::proxy::TransparentProxy) clones the `Arc`
108    /// into the listener config it builds while the original stays in its
109    /// template.
110    ///
111    /// So a leg holding both could do exactly one thing: take the spec,
112    /// count it as configured, and deliver it nowhere. The connection would
113    /// come up, the leg would report success, and the file the caller was
114    /// watching would never be written to at all — the failure this crate
115    /// exists to make impossible, in the one place a caller has no way of
116    /// noticing it.
117    ///
118    /// A caller who wants both attaches the sink to their own config with
119    /// `QlogSpec::attach_to(&mut base)` *before* the `Arc` is made — that is
120    /// the last moment a mutable reference to it exists — and sets the
121    /// result as this leg's raw config. A caller who has no config of their
122    /// own drops the field and lets the leg build one, which is what a leg
123    /// carrying a spec alone does.
124    #[cfg(feature = "qlog")]
125    #[error(
126        "the {leg:?} leg was given both a transport_config and a qlog spec, which cannot be \
127         combined: quinn installs a capture sink by mutating a quinn::TransportConfig, and this \
128         leg's config arrives behind an Arc that can be neither cloned nor mutated, so the spec \
129         would be accepted and delivered nowhere. Attach the sink to your own config with \
130         QlogSpec::attach_to(&mut base) before wrapping it in an Arc and set the result as this \
131         leg's transport_config, or drop the transport_config and let the leg build one"
132    )]
133    TransportConfigAndQlog {
134        /// The leg holding the contradiction.
135        leg: Leg,
136    },
137    /// A leg's [`QlogSpec`](crate::qlog::QlogSpec) could not become a
138    /// capture.
139    ///
140    /// The leg is refused rather than opened without the sink, for the
141    /// reason every refusal in this area exists: a connection that came up
142    /// anyway would run, report success, and leave the caller's capture
143    /// empty — and an empty capture is indistinguishable from a capture of
144    /// a connection that carried nothing, which is exactly the question a
145    /// capture is usually read to answer.
146    ///
147    /// Both of [`QlogError`](crate::qlog::QlogError)'s variants reach here.
148    /// `NoWriter` is a spec nobody finished writing, refused before a sink
149    /// is built or an endpoint exists; `NotStarted` is a writer that refused
150    /// the preamble, which is the first thing written and therefore the
151    /// first place a full disk or an unwritable path shows up.
152    #[cfg(feature = "qlog")]
153    #[error("the {leg:?} leg's qlog capture could not be started: {source}")]
154    Qlog {
155        /// The leg holding the spec.
156        leg: Leg,
157        /// Why the spec could not become a capture.
158        source: crate::qlog::QlogError,
159    },
160    /// A [`TransparentProxy`](crate::proxy::TransparentProxy) was built from
161    /// a template carrying a [`QlogSpec`](crate::qlog::QlogSpec), which it
162    /// cannot deliver to any leg.
163    ///
164    /// # Why a proxy template cannot carry one
165    ///
166    /// A `TransparentProxy` holds its two configurations as a **template**
167    /// and copies them: the listener's once, when it binds, and the
168    /// session's once per accepted connection. A `QlogSpec` owns a
169    /// `Box<dyn Write>`, has no `Clone`, and is consumed the moment it
170    /// becomes a sink, so there is nothing a copy could hand over — and one
171    /// writer cannot be divided between the connections a proxy accepts in
172    /// any case. One sink shared by two connections writes both into one
173    /// file, behind one preamble, with no record saying where the first ends.
174    ///
175    /// # Why it is refused rather than dropped
176    /// Because the alternative is this crate's cardinal failure with nothing to
177    /// give it away. A proxy that quietly left the field behind would bind,
178    /// accept, forward and report success, and the only symptom would be a file
179    /// that was never created — which a caller reads as *the run produced no
180    /// events* rather than as *the capture was never installed*. The
181    /// contradiction is knowable before a socket exists, so it is answered
182    /// there.
183    ///
184    /// The fix is to capture the leg where one writer per connection is
185    /// expressible: build the
186    /// [`ListenerConfig`](crate::listener::ListenerConfig) and call
187    /// [`Listener::bind`](crate::listener::Listener::bind) for the client
188    /// leg, or drive a [`ProxySession`](crate::session::ProxySession) with
189    /// its own spec for the relay leg.
190    #[cfg(feature = "qlog")]
191    #[error(
192        "a TransparentProxy template was given a qlog spec for the {leg:?} leg, which it cannot \
193         deliver: a proxy copies its configuration — the listener's once, the session's once per \
194         accepted connection — and a QlogSpec owns its writer, has no Clone and is consumed when \
195         it becomes a sink, so the copy would carry nothing and the capture would never be \
196         installed. Capture the client leg by building a ListenerConfig and calling \
197         Listener::bind, or the relay leg by driving a ProxySession, one spec per connection"
198    )]
199    QlogOnProxyTemplate {
200        /// The leg whose template holds the spec.
201        leg: Leg,
202    },
203    /// A leg's [`TransportProfile`](crate::transport::TransportProfile)
204    /// could not be turned into a `quinn::TransportConfig`.
205    ///
206    /// The leg is refused rather than opened with quinn's defaults: a
207    /// connection that came up anyway would run with parameters nobody
208    /// chose and report success, and the profile that was ignored is
209    /// precisely the record of what the run was supposed to be.
210    #[error("the {leg:?} leg's transport profile was refused: {source}")]
211    TransportProfile {
212        /// The leg holding the profile.
213        leg: Leg,
214        /// Why the profile could not be honoured.
215        source: TransportProfileError,
216    },
217    /// A shaping class keys on a field this session's draft does not carry,
218    /// so the class could never claim a unit.
219    ///
220    /// Refused before the session dials, rather than left to be discovered
221    /// from a report during the run. The rule is not merely unlikely to
222    /// match — on this draft there is no traffic at all that could satisfy
223    /// it, so a session carrying one would pace nothing the author asked
224    /// for, count itself as shaping, and finish clean. That is the whole
225    /// failure this crate exists to make impossible, arrived at through a
226    /// configuration file rather than a bug.
227    ///
228    /// Distinct from
229    /// [`ShapeError::InertMatcher`](crate::shape::ShapeError::InertMatcher),
230    /// and the distinction is where the answer lives: an empty value set is
231    /// a property of the configuration alone, so the constructor rejects it
232    /// with no draft in hand; a key the draft does not carry needs a draft
233    /// to judge, which only exists once a session is being started.
234    #[error("{source}")]
235    ShapeRuleUnsupported {
236        /// The class, the draft, the stream kind and the key.
237        #[from]
238        source: crate::capability::UnsupportedMatcherKey,
239    },
240    /// The session is configured for a MoQT draft this build did not
241    /// compile a codec for.
242    ///
243    /// [`DraftVersion`] carries all thirteen variants under every feature
244    /// set, so a draft that was never compiled is still a value a
245    /// configuration can hold — and
246    /// `ProxySessionConfig::default().draft` holds one of them. On a build
247    /// made with a reduced draft set, nothing about such a configuration
248    /// looks wrong.
249    ///
250    /// # What the session would do instead
251    ///
252    /// Run, and forward everything uninterpreted. The dispatch enums fall
253    /// through to their catch-all arm and answer
254    /// `CodecError::UnsupportedDraft`, which is not an
255    /// incomplete-input error, so the object framer takes its terminal
256    /// arm, latches [`BypassReason::DecodeError`](crate::framer::BypassReason)
257    /// and pumps the stream through as bytes. No object reaches a hook, no
258    /// shaping class claims anything, no `ProxyEvent::Object` is emitted —
259    /// and the run completes, reports success, and produces a stream of
260    /// zeroes that is indistinguishable from a session nothing was sent on.
261    /// Control frames fare no better and are quieter still: the control
262    /// parser skips a frame it cannot decode and emits nothing at all.
263    ///
264    /// So it is refused where the session starts, beside the shaping
265    /// admission check and before the relay is dialled, because a byte pump
266    /// reporting success is precisely what this crate exists to make
267    /// impossible.
268    ///
269    /// # It names the draft that was resolved, not the one configured
270    ///
271    /// Drafts 15 and later are settled by the client's ALPN, so the draft a
272    /// session will actually frame with may not be the one in its
273    /// configuration. This carries the resolved one, which is the one that
274    /// is missing.
275    ///
276    /// The fix is a build that carries the draft — the `draftNN` feature of
277    /// this crate, which forwards to both the codec and the client — or a
278    /// configuration naming one this build has.
279    #[error(
280        "this session is configured for {draft:?}, which this build did not compile: it would \
281         forward every stream uninterpreted, surface no object to any hook, claim nothing with \
282         any shaping class, and report success. Build with the matching draftNN feature, or \
283         configure a draft this build carries"
284    )]
285    DraftNotCompiled {
286        /// The draft this build cannot frame.
287        draft: DraftVersion,
288    },
289    /// TLS configuration error.
290    #[error("TLS config error: {0}")]
291    TlsConfig(String),
292    /// Certificate generation error.
293    #[error("certificate generation error: {0}")]
294    CertGen(String),
295    /// Session was closed.
296    #[error("session closed: {0}")]
297    SessionClosed(String),
298    /// Proxy is shutting down.
299    #[error("proxy shutdown")]
300    Shutdown,
301}