fix(python): surface typed remote resource errors (#4789)

## Summary

- Parse structured `error_code` and `resource` fields from remote HTTP
error bodies and carry them through Rust/PyO3.
- Add `SecretNotFoundError`, `FunctionNotFoundError`,
`NamespaceNotFoundError`, and `AlreadyExistsError` as `HttpError`
subclasses.
- Expose `error_code`, `resource_kind`, `resource_name`, and
`namespace_path` on HTTP exceptions while preserving `status_code`,
`request_id`, and existing `except HttpError` handlers.
- Do not retry structured domain errors that would otherwise become
`RetryError` (notably `already_exists` on 409).

Older servers that do not provide the structured fields continue to
raise a plain `HttpError`.

## Compatibility

This consumes the stable resource error contract added in
lancedb/sophon#9516 (merge commit `35858561b3`). The existing
`TableNotFoundError` behavior is unchanged.

Part of ENT-3373.
This commit is contained in:
Xuanwo
2026-11-02 05:11:38 +08:00
committed by GitHub
parent eeb244fa57
commit 9786f2b63b
7 changed files with 574 additions and 18 deletions
+36 -10
View File
@@ -2,13 +2,13 @@
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use pyo3::{
PyErr, PyResult, Python,
Bound, PyAny, PyErr, PyResult, Python,
exceptions::{PyIOError, PyNotImplementedError, PyOSError, PyRuntimeError, PyValueError},
intern,
types::{PyAnyMethods, PyNone},
};
use lancedb::error::Error as LanceError;
use lancedb::error::{Error as LanceError, HttpErrorDetails};
pub trait PythonErrorExt<T> {
/// Convert to a python error based on the Lance error type
@@ -65,14 +65,13 @@ impl<T> PythonErrorExt<T> for std::result::Result<T, LanceError> {
status_code,
} => Python::attach(|py| {
let message = err.to_string();
let http_err_cls = py
.import(intern!(py, "lancedb.remote.errors"))?
.getattr(intern!(py, "HttpError"))?;
let err = http_err_cls.call1((
let err = new_py_http_error(
py,
message,
request_id,
status_code.map(|s| s.as_u16()),
))?;
err.http_error_details(),
)?;
if let Some(cause) = source.source() {
// The HTTP error already includes the first cause. But
@@ -163,13 +162,15 @@ impl<T> PythonErrorExt<T> for std::result::Result<T, LanceError> {
fn http_from_rust_error(
py: Python<'_>,
err: &dyn std::error::Error,
err: &(dyn std::error::Error + 'static),
request_id: &str,
status_code: Option<u16>,
) -> PyResult<PyErr> {
let message = err.to_string();
let http_err_cls = py.import("lancedb.remote.errors")?.getattr("HttpError")?;
let py_err = http_err_cls.call1((message, request_id, status_code))?;
let details = err
.downcast_ref::<LanceError>()
.and_then(LanceError::http_error_details);
let py_err = new_py_http_error(py, message, request_id, status_code, details)?;
// Reset the traceback since it doesn't provide additional information.
let py_err = py_err.call_method1(intern!(py, "with_traceback"), (PyNone::get(py),))?;
@@ -181,3 +182,28 @@ fn http_from_rust_error(
Ok(PyErr::from_value(py_err))
}
fn new_py_http_error<'py>(
py: Python<'py>,
message: String,
request_id: &str,
status_code: Option<u16>,
details: Option<&HttpErrorDetails>,
) -> PyResult<Bound<'py, PyAny>> {
let error_code = details.map(|details| details.error_code.as_str());
let resource_kind = details.map(|details| details.resource_kind.as_str());
let resource_name = details.map(|details| details.resource_name.as_str());
let namespace_path = details.map(|details| details.namespace_path.clone());
py.import("lancedb.remote.errors")?
.getattr("HttpError")?
.call1((
message,
request_id,
status_code,
error_code,
resource_kind,
resource_name,
namespace_path,
))
}