Files
lancedb/python/python/tests/test_errors.py
T
Xuanwo 9786f2b63b 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.
2026-11-02 05:11:38 +08:00

193 lines
5.7 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import pickle
import pytest
from lance_namespace.errors import ErrorCode
from lance_namespace.errors import TableNotFoundError as NamespaceTableNotFoundError
from lancedb.db import DBConnection
from lancedb.remote.errors import (
AlreadyExistsError,
FunctionNotFoundError,
HttpError,
LanceDBClientError,
NamespaceNotFoundError,
RetryError,
SecretNotFoundError,
TableNotFoundError,
)
from lancedb.table import Table
@pytest.mark.parametrize(
"base, method",
[
(Table, "to_lance"),
(Table, "to_polars"),
(DBConnection, "drop_all_tables"),
(DBConnection, "drop_database"),
],
)
def test_base_unsupported_methods_have_messages(base, method):
with pytest.raises(NotImplementedError) as exc_info:
getattr(base, method)(None)
message = str(exc_info.value)
assert method in message
assert "not supported" in message
def test_pickle_lancedb_client_error():
err = LanceDBClientError("something went wrong", "req-123", 400)
restored = pickle.loads(pickle.dumps(err))
assert str(restored) == "something went wrong"
assert restored.request_id == "req-123"
assert restored.status_code == 400
def test_pickle_lancedb_client_error_no_status_code():
err = LanceDBClientError("fail", "req-456")
restored = pickle.loads(pickle.dumps(err))
assert str(restored) == "fail"
assert restored.request_id == "req-456"
assert restored.status_code is None
def test_pickle_http_error():
err = HttpError("not found", "req-789", 404)
restored = pickle.loads(pickle.dumps(err))
assert isinstance(restored, HttpError)
assert str(restored) == "not found"
assert restored.request_id == "req-789"
assert restored.status_code == 404
@pytest.mark.parametrize(
"error_code,resource_kind,status_code,expected_type",
[
("not_found", "secret", 404, SecretNotFoundError),
("not_found", "function", 404, FunctionNotFoundError),
("not_found", "namespace", 404, NamespaceNotFoundError),
("already_exists", "secret", 409, AlreadyExistsError),
],
)
def test_http_error_structured_resource_error(
error_code, resource_kind, status_code, expected_type
):
err = HttpError(
"resource request failed",
"req-structured",
status_code,
error_code,
resource_kind,
"resource-name",
["parent", "child"],
)
assert type(err) is expected_type
assert isinstance(err, HttpError)
assert err.error_code == error_code
assert err.resource_kind == resource_kind
assert err.resource_name == "resource-name"
assert err.namespace_path == ("parent", "child")
assert err.request_id == "req-structured"
assert err.status_code == status_code
@pytest.mark.parametrize(
"error_code,resource_kind",
[
(None, None),
("not_found", None),
("not_found", "unknown"),
("invalid", "secret"),
],
)
def test_http_error_without_known_structured_fields_stays_generic(
error_code, resource_kind
):
err = HttpError(
"not found",
"req-generic",
404,
error_code,
resource_kind,
"resource-name",
[],
)
assert type(err) is HttpError
assert err.error_code == error_code
assert err.resource_kind == resource_kind
@pytest.mark.parametrize(
"exception_type,error_code,resource_kind",
[
(SecretNotFoundError, "not_found", "secret"),
(FunctionNotFoundError, "not_found", "function"),
(NamespaceNotFoundError, "not_found", "namespace"),
(AlreadyExistsError, "already_exists", "namespace"),
],
)
def test_pickle_structured_http_error(exception_type, error_code, resource_kind):
err = HttpError(
"resource request failed",
"req-pickle",
409 if error_code == "already_exists" else 404,
error_code,
resource_kind,
"resource-name",
["parent"],
)
assert type(err) is exception_type
restored = pickle.loads(pickle.dumps(err))
assert type(restored) is exception_type
assert restored.error_code == error_code
assert restored.resource_kind == resource_kind
assert restored.resource_name == "resource-name"
assert restored.namespace_path == ("parent",)
assert restored.request_id == "req-pickle"
assert restored.status_code == err.status_code
def test_pickle_table_not_found_error():
err = TableNotFoundError("Table 'missing' was not found", "req-missing", 404)
restored = pickle.loads(pickle.dumps(err))
assert isinstance(restored, TableNotFoundError)
assert isinstance(restored, NamespaceTableNotFoundError)
assert isinstance(restored, ValueError)
assert isinstance(restored, HttpError)
assert restored.code == ErrorCode.TABLE_NOT_FOUND
assert str(restored) == "Table 'missing' was not found"
assert restored.request_id == "req-missing"
assert restored.status_code == 404
def test_pickle_retry_error():
err = RetryError(
"max retries exceeded",
"req-abc",
request_failures=3,
connect_failures=1,
read_failures=2,
max_request_failures=5,
max_connect_failures=3,
max_read_failures=3,
status_code=503,
)
restored = pickle.loads(pickle.dumps(err))
assert isinstance(restored, RetryError)
assert str(restored) == "max retries exceeded"
assert restored.request_id == "req-abc"
assert restored.request_failures == 3
assert restored.connect_failures == 1
assert restored.read_failures == 2
assert restored.max_request_failures == 5
assert restored.max_connect_failures == 3
assert restored.max_read_failures == 3
assert restored.status_code == 503