Skip to content

Commit c4cbcce

Browse files
committed
fix(hiroz-py): stop boxing anyhow errors before timeout classification
Converting anyhow::Error into Box<dyn Error> wraps the value, so is_timeout's downcast no longer sees the real error and every service timeout was misreported as a plain HirozError -- caught by test_p5_call_timeout_raises_timeout_error. Split into map_call_error (anyhow, derefs) and map_zenoh_error (the action paths' Box<dyn Error>), sharing the classify/format helpers.
1 parent d1cba6e commit c4cbcce

2 files changed

Lines changed: 28 additions & 23 deletions

File tree

crates/hiroz-py/src/action.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ impl PyZActionClient {
192192
"send_goal timed out: no action server responded",
193193
)
194194
})?
195-
.map_err(crate::error::map_call_error)
195+
.map_err(crate::error::map_zenoh_error)
196196
})
197197
})?;
198198

@@ -360,14 +360,14 @@ impl PyZClientGoalHandle {
360360
"Action result not received within {t:?}"
361361
)))
362362
}
363-
Err(e) => Err(crate::error::map_call_error(e)),
363+
Err(e) => Err(crate::error::map_zenoh_error(e)),
364364
}
365365
} else {
366366
handle
367367
.result()
368368
.await
369369
.map(|msg| msg.0)
370-
.map_err(crate::error::map_call_error)
370+
.map_err(crate::error::map_zenoh_error)
371371
}
372372
})
373373
})?;

crates/hiroz-py/src/error.rs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pyo3::create_exception!(hiroz_py, TypeMismatchError, HirozError);
1010

1111
/// Render an error and its full source chain as `outer: inner: root`.
1212
///
13-
/// Matches anyhow's `{:#}` output, which we lose once the error is boxed.
13+
/// Matches anyhow's `{:#}` output, which a bare `Box<dyn Error>` does not give us.
1414
fn format_chain(err: &(dyn std::error::Error + 'static)) -> String {
1515
let mut msg = err.to_string();
1616
let mut source = err.source();
@@ -21,31 +21,36 @@ fn format_chain(err: &(dyn std::error::Error + 'static)) -> String {
2121
msg
2222
}
2323

24-
/// Map a core error to the right Python exception.
25-
///
26-
/// Timeout-shaped errors become `hiroz_py.TimeoutError`; everything else
27-
/// becomes `hiroz_py.HirozError`. Use this for blocking calls that raise on
28-
/// failure (e.g. `ZClient.call`, `send_goal`, `get_result`). Methods whose
29-
/// documented contract is to return `None` on timeout should keep doing so
30-
/// rather than calling this.
31-
///
32-
/// Generic over the error type because the service path yields `anyhow::Error`
33-
/// while the action path yields `zenoh::Error`; both classify identically via
34-
/// the core's structured detector, which walks the whole source chain — do not
35-
/// string-match on the message.
36-
pub(crate) fn map_call_error<E>(e: E) -> PyErr
37-
where
38-
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
39-
{
40-
let err = e.into();
41-
let msg = format_chain(&*err);
42-
if hiroz::error::is_timeout(&*err) {
24+
fn classify(is_timeout: bool, msg: String) -> PyErr {
25+
if is_timeout {
4326
TimeoutError::new_err(msg)
4427
} else {
4528
HirozError::new_err(msg)
4629
}
4730
}
4831

32+
/// Map an `anyhow` error to the right Python exception.
33+
///
34+
/// Timeout-shaped errors become `hiroz_py.TimeoutError`; everything else
35+
/// becomes `hiroz_py.HirozError`. Use this for blocking calls that raise on
36+
/// failure (e.g. `ZClient.call`). Methods whose documented contract is to
37+
/// return `None` on timeout should keep doing so rather than calling this.
38+
///
39+
/// Classification goes through the core's structured detector, which walks the
40+
/// whole source chain — do not string-match on the message.
41+
pub(crate) fn map_call_error(e: anyhow::Error) -> PyErr {
42+
// Deref rather than boxing: `Box<dyn Error>::from(anyhow::Error)` wraps the
43+
// value so `is_timeout`'s downcast no longer sees the real error and every
44+
// timeout is misreported as a plain HirozError.
45+
classify(hiroz::error::is_timeout(&*e), format!("{e:#}"))
46+
}
47+
48+
/// Same mapping for the action paths, which yield `zenoh::Error`
49+
/// (`Box<dyn Error + Send + Sync>`) rather than `anyhow::Error`.
50+
pub(crate) fn map_zenoh_error(e: zenoh::Error) -> PyErr {
51+
classify(hiroz::error::is_timeout(&*e), format_chain(&*e))
52+
}
53+
4954
/// Trait for converting Rust errors to Python exceptions
5055
pub(crate) trait IntoPyErr {
5156
fn into_pyerr(self) -> PyErr;

0 commit comments

Comments
 (0)