@smbcloud / cli / commits / 1f423c6

fix: handle numeric ControlKit method error codes

Seto Elkahfi committed Aug 25, 2026 at 14:12 UTC 1f423c632228997e94e50725617157a6ece5b7dd
1 file changed +55 -35
crates/xcrs/src/xcrs.rs
+55 -35
@@ -135,41 +135,40 @@ impl ControlKit {
135 let body = self.send(method, params).await?;
136
137 if let Some(error) = body.get("error") {
138 - let (runner_info, runner_info_error) =
139 - if error.get("code").and_then(serde_json::Value::as_i64)
140 - == Some(CONTROLKIT_METHOD_NOT_FOUND)
141 - && method != "device.info"
138 + // `send` cannot recurse; this guard only avoids repeating an unsupported probe.
139 + let should_probe_runner =
140 + is_controlkit_method_not_found(error) && method != "device.info";
141 + let (runner_info, runner_info_error) = if should_probe_runner {
142 + match tokio::time::timeout(
143 + CONTROLKIT_RUNNER_INFO_TIMEOUT,
144 + self.send("device.info", serde_json::json!({})),
145 + )
146 + .await
147 {
143 - match tokio::time::timeout(
144 - CONTROLKIT_RUNNER_INFO_TIMEOUT,
145 - self.send("device.info", serde_json::json!({})),
146 - )
147 - .await
148 - {
149 - Ok(Ok(body)) => match body.get("result") {
150 - Some(result) => (Some(result.clone()), None),
151 - None => {
152 - let reason = body
153 - .get("error")
154 - .map(|error| format!("device.info returned {error}"))
155 - .unwrap_or_else(|| {
156 - "device.info response did not contain a result".to_string()
157 - });
158 - (None, Some(reason))
159 - }
160 - },
161 - Ok(Err(error)) => (None, Some(error.to_string())),
162 - Err(_) => (
163 - None,
164 - Some(format!(
165 - "device.info timed out after {} seconds",
166 - CONTROLKIT_RUNNER_INFO_TIMEOUT.as_secs()
167 - )),
168 - ),
169 - }
170 - } else {
171 - (None, None)
172 - };
148 + Ok(Ok(body)) => match body.get("result") {
149 + Some(result) => (Some(result.clone()), None),
150 + None => {
151 + let reason = body
152 + .get("error")
153 + .map(|error| format!("device.info returned {error}"))
154 + .unwrap_or_else(|| {
155 + "device.info response did not contain a result".to_string()
156 + });
157 + (None, Some(reason))
158 + }
159 + },
160 + Ok(Err(error)) => (None, Some(error.to_string())),
161 + Err(_) => (
162 + None,
163 + Some(format!(
164 + "device.info timed out after {} seconds",
165 + CONTROLKIT_RUNNER_INFO_TIMEOUT.as_secs()
166 + )),
167 + ),
168 + }
169 + } else {
170 + (None, None)
171 + };
172 return Err(anyhow!(controlkit_rpc_error(
173 method,
174 error,
@@ -212,6 +211,15 @@ impl ControlKit {
211 }
212 }
213
214 +fn is_controlkit_method_not_found(error: &serde_json::Value) -> bool {
215 + let Some(code) = error.get("code") else {
216 + return false;
217 + };
218 +
219 + code.as_i64() == Some(CONTROLKIT_METHOD_NOT_FOUND)
220 + || code.as_f64() == Some(CONTROLKIT_METHOD_NOT_FOUND as f64)
221 +}
222 +
223 fn controlkit_rpc_error(
224 method: &str,
225 error: &serde_json::Value,
@@ -227,7 +235,7 @@ fn controlkit_rpc_error(
235 .and_then(serde_json::Value::as_str)
236 .unwrap_or("unknown JSON-RPC error");
237
230 - if code != CONTROLKIT_METHOD_NOT_FOUND {
238 + if !is_controlkit_method_not_found(error) {
239 return format!("ControlKit method '{method}' failed ({code}): {message}");
240 }
241
@@ -1381,6 +1389,18 @@ mod tests {
1389 assert!(message.contains("Rebuild or upgrade"));
1390 }
1391
1392 + #[test]
1393 + fn recognizes_integral_float_controlkit_method_not_found_code() {
1394 + let error = serde_json::json!({
1395 + "code": -32601.0,
1396 + "message": "Method not found"
1397 + });
1398 +
1399 + assert!(is_controlkit_method_not_found(&error));
1400 + let message = controlkit_rpc_error("device.dump.ui", &error, None, None);
1401 + assert!(message.contains("does not implement ControlKit method 'device.dump.ui'"));
1402 + }
1403 +
1404 #[test]
1405 fn reports_when_controlkit_runner_metadata_is_unavailable() {
1406 let error = serde_json::json!({