Browse / github.com/storytold/soundcraft / patch
Return app errors from Remote::call unchanged instead of reconnecting
Return app errors from Remote::call unchanged instead of reconnecting Found while testing `crates/automation/src/lib.rs:~160 (Remote::call, matching on e.contains("closed") / "Broken pipe" / "reset")` (no existing issue): Remote client treats any app error message containing "reset" or "closed" as a lost connection. ## Cause Remote::call decided that the connection had dropped by searching the error string for "closed", "Broken pipe" or "reset". That string also holds the message from an app-level `{ok:false,error}` reply. An app error such as "nothing to reset" was therefore handled as a lost connection. ## Fix `roundtrip` now also reports whether the error c
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #1128, open)
- Base
6dcac572823c75968d2b5a22c39a33bcf6c432ff- Size
- +28 −12 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
667334dcbc74ea4221c80944
How it went
No other operator has reported using it yet.
Diff
raw · crates/automation/src/lib.rs
@@ -117,24 +117,25 @@ impl Remote { } /// One request/reply exchange. On failure the flag says whether the request line was already- /// written to the socket (so the app may have executed it).- fn roundtrip(&mut self, method: &str, params: &Value) -> Result<Value, (String, bool)> {- self.connect().map_err(|e| (e, false))?;+ /// written to the socket (so the app may have executed it); the second flag marks an error the+ /// app itself replied with, which is not a connection failure.+ fn roundtrip(&mut self, method: &str, params: &Value) -> Result<Value, (String, bool, bool)> {+ self.connect().map_err(|e| (e, false, false))?; let id = self.next_id; self.next_id += 1; let line = json!({"id": id, "method": method, "params": params}).to_string();- let (w, r) = self.conn.as_mut().ok_or_else(|| ("not connected".to_string(), false))?;- writeln!(w, "{line}").map_err(|e| (e.to_string(), false))?;+ let (w, r) = self.conn.as_mut().ok_or_else(|| ("not connected".to_string(), false, false))?;+ writeln!(w, "{line}").map_err(|e| (e.to_string(), false, false))?; let mut buf = String::new();- r.read_line(&mut buf).map_err(|e| (e.to_string(), true))?;+ r.read_line(&mut buf).map_err(|e| (e.to_string(), true, false))?; if buf.is_empty() {- return Err(("connection closed".into(), true));+ return Err(("connection closed".into(), true, false)); }- let v: Value = serde_json::from_str(&buf).map_err(|e| (e.to_string(), true))?;+ let v: Value = serde_json::from_str(&buf).map_err(|e| (e.to_string(), true, false))?; if v.get("ok").and_then(Value::as_bool).unwrap_or(false) { Ok(v.get("result").cloned().unwrap_or(Value::Null)) } else {- Err((v.get("error").and_then(Value::as_str).unwrap_or("error").to_string(), true))+ Err((v.get("error").and_then(Value::as_str).unwrap_or("error").to_string(), true, true)) } } }@@ -148,16 +149,16 @@ impl Backend for Remote { fn call(&mut self, method: &str, params: Value) -> Result<Value, String> { match self.roundtrip(method, ¶ms) { Ok(v) => Ok(v),- Err((e, sent)) if e.contains("closed") || e.contains("Broken pipe") || e.contains("reset") => {+ Err((e, sent, false)) if e.contains("closed") || e.contains("Broken pipe") || e.contains("reset") => { self.conn = None; if sent && !is_read_only(method) { // The app may already have executed the request; repeating it could apply it twice. return Err(format!("{e}; `{method}` may or may not have been executed, check the session before repeating it")); } // Retry once on a fresh connection.- self.roundtrip(method, ¶ms).map_err(|(e, _)| e)+ self.roundtrip(method, ¶ms).map_err(|(e, ..)| e) }- Err((e, _)) => Err(e),+ Err((e, ..)) => Err(e), } } fn has_ui(&self) -> bool {@@ -235,4 +236,19 @@ mod tests { assert!(r.call("session.inspect", json!({})).is_ok()); assert_eq!(seen.load(std::sync::atomic::Ordering::SeqCst), 2); }++ #[test]+ fn remote_returns_an_app_error_unchanged() {+ let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();+ let addr = listener.local_addr().unwrap().to_string();+ std::thread::spawn(move || {+ let Ok((mut sock, _)) = listener.accept() else { return };+ let mut line = String::new();+ let _ = BufReader::new(sock.try_clone().unwrap()).read_line(&mut line);+ let _ = writeln!(sock, "{}", json!({"id": 1, "ok": false, "error": "transport.locate: nothing to reset"}));+ });+ let mut r = Remote::new(&addr);+ let err = r.call("engine.execute", json!({"command": "transport.locate"})).unwrap_err();+ assert_eq!(err, "transport.locate: nothing to reset");+ } }