diff --git a/src/uplink/webrtc.rs b/src/uplink/webrtc.rs index 396f891..583c082 100644 --- a/src/uplink/webrtc.rs +++ b/src/uplink/webrtc.rs @@ -62,6 +62,29 @@ fn checked_stream_name(name: &str) -> anyhow::Result<&str> { Ok(name) } +/// Flattens an error's `source` chain into one line. +/// +/// Needed because reqwest's own `Display` is just "error sending request for +/// url (...)" - the reason lives one or two sources down, and "connection +/// refused" versus "timed out" is the whole content of the message. This +/// error travels to the cloud and then to a viewer, so it is the only +/// explanation anyone gets. +fn with_causes(err: &(dyn std::error::Error + 'static)) -> String { + let mut out = err.to_string(); + let mut source = err.source(); + while let Some(cause) = source { + let text = cause.to_string(); + // Skip a cause that only repeats what we already said, which hyper's + // wrappers do. + if !out.contains(&text) { + out.push_str(": "); + out.push_str(&text); + } + source = cause.source(); + } + out +} + /// Posts the browser's offer to go2rtc and returns its answer SDP. pub async fn negotiate(api_listen: &str, camera_stream: &str, offer_sdp: &str) -> anyhow::Result { let base = local_api_base(api_listen)?; @@ -73,7 +96,13 @@ pub async fn negotiate(api_listen: &str, camera_stream: &str, offer_sdp: &str) - .body(offer_sdp.to_string()) .timeout(NEGOTIATION_TIMEOUT) .send() - .await?; + .await + .map_err(|err| { + // Names go2rtc explicitly. The most likely cause by far is that + // it is not running, and an error mentioning only a port sends + // whoever reads it looking at the network. + anyhow::anyhow!("could not reach go2rtc at {base}: {}", with_causes(&err)) + })?; // go2rtc answers 201 rather than 200 here, so this checks the class and // not a single code.