---
description: 了解和处理 OpenSandbox Python SDK 返回的错误,包括异常层级、错误码和最佳实践。
---
# 错误处理
OpenSandbox Python SDK 提供了一套结构化的分层错误系统,便于在应用中识别、分类和处理错误。本指南涵盖完整的异常层级、服务端错误码以及实际使用示例。
## 异常层级
所有 SDK 异常均继承自 `SandboxException`,形成清晰的层级结构,支持任意粒度的捕获:
```
SandboxException ← 所有 SDK 错误的根异常
├── SandboxApiException ← API 返回 HTTP 4xx/5xx
├── SandboxInternalException ← SDK 内部 / 网络错误
├── SandboxUnhealthyException ← sandbox 健康检查失败
├── SandboxReadyTimeoutException ← sandbox 就绪超时
├── InvalidArgumentException ← SDK 输入参数无效
├── PoolEmptyException ← 池中没有空闲 sandbox
├── PoolAcquireFailedException ← 池获取失败
├── PoolStateStoreUnavailableException ← 状态存储不可用
├── PoolStateStoreContentionException ← 状态存储竞争
└── PoolNotRunningException ← 池未运行
```
每个异常都包含以下字段:
| 字段 | 类型 | 说明 |
|-------|------|------|
| `error.code` | `str` | 机器可读的错误码(如 `INTERNAL_UNKNOWN_ERROR`、`DOCKER::SANDBOX_NOT_FOUND`) |
| `error.message` | `str \| None` | 人类可读的错误描述 |
| `request_id` | `str \| None` | 服务端请求追踪 ID(仅在 `SandboxApiException` 上可用) |
| `__cause__` | `Exception \| None` | 被转换的原始异常 |
| `status_code` | `int \| None` | HTTP 状态码(仅在 `SandboxApiException` 上可用) |
### 按粒度捕获
```python
from opensandbox.exceptions import (
SandboxException, # 捕获所有
SandboxApiException, # API 级错误
SandboxReadyTimeoutException, # 就绪超时
PoolEmptyException, # 池相关
)
# 方式 1:捕获所有异常
try:
sandbox = await Sandbox.create("ubuntu", connection_config=config)
except SandboxException as e:
print(f"[{e.error.code}] {e.error.message}")
# 方式 2:对不同错误分别处理
try:
sandbox = await Sandbox.create("ubuntu", connection_config=config)
except SandboxReadyTimeoutException:
print("Sandbox 就绪超时 — 请增加超时时间后重试")
except SandboxApiException as e:
if e.status_code == 404:
print("服务端未找到该 Sandbox")
else:
print(f"API 错误 HTTP {e.status_code}: [{e.error.code}] {e.error.message}")
except SandboxException as e:
print(f"其他 SDK 错误: [{e.error.code}] {e.error.message}")
```
## 异常类型详解
### SandboxApiException
当 OpenSandbox 服务端返回 HTTP 错误(4xx 或 5xx)时抛出。SDK 的 `ExceptionConverter` 解析服务端的 `{"code": "...", "message": "..."}` 响应体,填充 `error.code` 和 `error.message`。`request_id` 字段来自服务端的 `X-Request-ID` 响应头,可用于日志关联。
```python
except SandboxApiException as e:
print(f"HTTP {e.status_code}") # 如 404
print(f"错误码: {e.error.code}") # 如 "DOCKER::SANDBOX_NOT_FOUND"
print(f"错误信息: {e.error.message}") # 如 "Sandbox xxx not found"
print(f"请求 ID: {e.request_id}") # 如 "req-abc123"
```
### SandboxInternalException
因 SDK 层面的故障(非服务端引起)而抛出。包括网络连接错误、httpx 传输错误(`ConnectError`、`TimeoutException`、`ReadTimeout`、`WriteTimeout`)、`IOError`/`OSError` 以及 `NotImplementedError`。
```python
except SandboxInternalException as e:
# 通常由网络问题引起
print(f"内部错误: [{e.error.code}] {e.error.message}")
# e.error.code 值始终为 INTERNAL_UNKNOWN_ERROR
```
### SandboxUnhealthyException
当 sandbox 在 `Sandbox.create()` 过程中健康检查失败时抛出。默认健康检查为对 execd 端口的 TCP ping;可自定义健康检查来覆盖此行为。
```python
except SandboxUnhealthyException as e:
print(f"Sandbox 不健康: {e.error.message}")
```
### SandboxReadyTimeoutException
当 sandbox 在 `ready_timeout`(默认 30 秒)内未能就绪时抛出。可在 `Sandbox.create()` 中增加超时时间或使用自定义健康检查来解决。
```python
from datetime import timedelta
# 增加超时时间以避免此异常
sandbox = await Sandbox.create(
"ubuntu",
connection_config=config,
ready_timeout=timedelta(minutes=2), # 更长的超时时间
)
```
### InvalidArgumentException
当 SDK 方法参数无效时抛出。将 `ValueError`、`TypeError` 和 pydantic `ValidationError` 包装到 SDK 异常层级中。
```python
except InvalidArgumentException as e:
print(f"无效输入: {e.error.message}")
# e.__cause__ 是原始的 ValueError/TypeError/ValidationError
```
### 池异常
以下异常与 `SandboxPool` API 相关:
| 异常 | 发生时机 |
|------|----------|
| `PoolEmptyException` | `acquire()` 使用 `FAIL_FAST` 策略,但没有空闲 sandbox |
| `PoolAcquireFailedException` | `acquire()` 使用 `FAIL_FAST` 策略,空闲候选不可用 |
| `PoolStateStoreUnavailableException` | Redis 或内存状态存储不可达 |
| `PoolStateStoreContentionException` | 原子存储操作遇到竞争 |
| `PoolNotRunningException` | 在池未运行时调用 `acquire()` |
```python
from opensandbox.exceptions import PoolEmptyException
try:
sandbox = pool.acquire(policy=AcquirePolicy.FAIL_FAST)
except PoolEmptyException:
print("没有空闲 sandbox — 请尝试 WAIT 策略或直接创建")
```
## 服务端错误码
服务端以标准 JSON 格式返回错误:
```json
{
"code": "DOCKER::SANDBOX_NOT_FOUND",
"message": "Sandbox 'abc123' not found"
}
```
SDK 的 `ExceptionConverter` 会自动将其解析为 `SandboxApiException` 上的 `SandboxError`。以下是完整的服务端错误码目录。
### Docker 运行时
| 错误码 | 典型 HTTP | 含义 |
|------|----------|------|
| `DOCKER::INITIALIZATION_ERROR` | 500 | Docker 运行时初始化失败 |
| `DOCKER::SANDBOX_QUERY_FAILED` | 500 | 容器查询失败 |
| `DOCKER::SANDBOX_NOT_FOUND` | 404 | Sandbox ID 不存在 |
| `DOCKER::SANDBOX_IMAGE_PULL_FAILED` | 500 | Docker 镜像拉取失败 |
| `DOCKER::SNAPSHOT_IMAGE_REMOVE_FAILED` | 500 | 快照镜像删除失败 |
| `DOCKER::SANDBOX_START_FAILED` | 500 | 容器启动失败 |
| `DOCKER::SANDBOX_DELETE_FAILED` | 500 | 容器删除失败 |
| `DOCKER::SANDBOX_NOT_RUNNING` | 400 | Sandbox 未处于运行状态 |
| `DOCKER::SANDBOX_PAUSE_FAILED` | 500 | 暂停操作失败 |
| `DOCKER::SANDBOX_NOT_PAUSED` | 400 | Sandbox 未处于暂停状态 |
| `DOCKER::SANDBOX_RESUME_FAILED` | 500 | 恢复操作失败 |
| `DOCKER::INVALID_EXPIRATION` | 400 | 过期时间值无效 |
| `DOCKER::EXPIRATION_NOT_EXTENDED` | 400 | 过期时间无法延长 |
| `DOCKER::SANDBOX_EXECD_START_FAILED` | 500 | Sandbox 内 exec daemon 启动失败 |
| `DOCKER::SANDBOX_EXECD_DISTRIBUTION_FAILED` | 500 | exec daemon 分发失败 |
| `DOCKER::SANDBOX_BOOTSTRAP_INSTALL_FAILED` | 500 | bootstrap 安装失败 |
| `DOCKER::INVALID_ENTRYPOINT` | 400 | 入口命令无效 |
| `DOCKER::INVALID_PORT` | 400 | 端口号无效 |
| `DOCKER::NETWORK_MODE_ENDPOINT_UNAVAILABLE` | 400 | 当前网络模式下端点不可用 |
### Kubernetes 运行时
| 错误码 | 典型 HTTP | 含义 |
|------|----------|------|
| `KUBERNETES::INITIALIZATION_ERROR` | 500 | K8s 运行时初始化失败 |
| `KUBERNETES::SANDBOX_NOT_FOUND` | 404 | Sandbox 不存在 |
| `KUBERNETES::POD_FAILED` | 500 | Pod 进入失败状态 |
| `KUBERNETES::POD_READY_TIMEOUT` | 500 | Pod 在超时时间内未就绪 |
| `KUBERNETES::API_ERROR` | 500 | K8s API 服务器调用失败 |
| `KUBERNETES::POD_IP_NOT_AVAILABLE` | 500 | Pod IP 尚未分配 |
| `KUBERNETES::INVALID_STATE` | 400 | 暂停/恢复状态转换无效 |
### 池
| 错误码 | 典型 HTTP | 含义 |
|------|----------|------|
| `KUBERNETES::POOL_NOT_FOUND` | 404 | 池名称不存在 |
| `KUBERNETES::POOL_ALREADY_EXISTS` | 409 | 池名称已存在 |
| `KUBERNETES::POOL_API_ERROR` | 500 | 池 API 调用失败 |
| `KUBERNETES::POOL_NOT_SUPPORTED` | 400 | 当前运行时不支持池 |
### 卷
| 错误码 | 典型 HTTP | 含义 |
|------|----------|------|
| `VOLUME::INVALID_NAME` | 400 | 卷名称无效 |
| `VOLUME::DUPLICATE_NAME` | 400 | 卷名称重复 |
| `VOLUME::INVALID_BACKEND` | 400 | 卷后端类型无效 |
| `VOLUME::INVALID_MOUNT_PATH` | 400 | 挂载路径无效 |
| `VOLUME::INVALID_SUB_PATH` | 400 | 子路径无效 |
| `VOLUME::INVALID_HOST_PATH` | 400 | 主机路径无效 |
| `VOLUME::HOST_PATH_NOT_ALLOWED` | 400 | 主机路径不在允许列表中 |
| `VOLUME::HOST_PATH_NOT_FOUND` | 400 | 主机路径在主机上不存在 |
| `VOLUME::HOST_PATH_CREATE_FAILED` | 500 | 主机路径创建失败 |
| `VOLUME::PVC_NOT_FOUND` | 404 | PersistentVolumeClaim 不存在 |
| `VOLUME::PVC_INSPECT_FAILED` | 500 | PVC 检查失败 |
| `VOLUME::PVC_SUBPATH_UNSUPPORTED_DRIVER` | 400 | 该 CSI 驱动不支持 PVC 子路径 |
| `VOLUME::UNSUPPORTED_BACKEND` | 400 | 不支持的后端类型 |
| `VOLUME::INVALID_OSSFS_VERSION` | 400 | OSSFS 版本无效 |
| `VOLUME::INVALID_OSSFS_ENDPOINT` | 400 | OSSFS 端点无效 |
| `VOLUME::INVALID_OSSFS_BUCKET` | 400 | OSSFS Bucket 名称无效 |
| `VOLUME::INVALID_OSSFS_OPTION` | 400 | OSSFS 挂载选项无效 |
| `VOLUME::INVALID_OSSFS_CREDENTIALS` | 400 | OSSFS 凭证无效 |
| `VOLUME::INVALID_OSSFS_MOUNT_ROOT` | 400 | OSSFS 挂载根路径无效 |
| `VOLUME::OSSFS_PATH_NOT_FOUND` | 400 | OSSFS 路径不存在 |
| `VOLUME::OSSFS_MOUNT_FAILED` | 500 | OSSFS 挂载操作失败 |
| `VOLUME::OSSFS_UNMOUNT_FAILED` | 500 | OSSFS 卸载操作失败 |
### 快照
| 错误码 | 典型 HTTP | 含义 |
|------|----------|------|
| `SNAPSHOT::INVALID_SOURCE_STATE` | 400 | 源 Sandbox 状态不适合创建快照 |
### 通用
| 错误码 | 典型 HTTP | 含义 |
|------|----------|------|
| `SANDBOX::UNKNOWN_ERROR` | 500 | 未分类错误的后备 |
| `SANDBOX::API_NOT_SUPPORTED` | 400 | 当前运行时不支持此 API |
| `SANDBOX::INVALID_PARAMETER` | 400 | 请求参数无效 |
| `SANDBOX::INVALID_METADATA_LABEL` | 400 | 元数据标签键无效 |
## 执行流错误
在流式命令执行过程中,sandbox 内部的错误以 `ServerStreamEventError` 事件形式传递,而非 SDK 异常。这些代表的是 sandbox 中运行的**用户代码**错误,而非 SDK 或服务端错误。
```python
from opensandbox.models.execd import ExecutionHandlers
async def handle_error(error_event):
print(f"错误类型: {error_event.ename}") # 如 "NameError"
print(f"错误信息: {error_event.evalue}") # 如 "name 'x' is not defined"
for line in error_event.traceback: # 堆栈跟踪行
print(line)
handlers = ExecutionHandlers(on_error=handle_error)
result = await sandbox.commands.run("undefined_var", handlers=handlers)
```
## 错误转换管道
SDK 通过 `ExceptionConverter` 自动将原始异常转换为 `SandboxException` 层级结构。无需直接调用,但了解转换规则有助于调试:
| 原始异常 | 转换为 | 典型场景 |
|----------|--------|----------|
| `UnexpectedStatus`(生成的 API 客户端) | `SandboxApiException` | API 返回未文档化的 HTTP 状态 |
| `httpx.HTTPStatusError` | `SandboxApiException` | API 返回文档化的错误状态 |
| `IOError` / `OSError` / `ConnectionError` | `SandboxInternalException` | 网络连接失败 |
| `httpx.ConnectError` / `httpx.TimeoutException` / `httpx.NetworkError` / `httpx.ReadTimeout` / `httpx.WriteTimeout` | `SandboxInternalException` | httpx 传输层故障 |
| `pydantic.ValidationError` | `InvalidArgumentException` | SDK 模型输入验证失败 |
| `ValueError` / `TypeError` | `InvalidArgumentException` | 无效的用户输入 |
| `NotImplementedError` | `SandboxInternalException` | 不支持的 SDK 操作 |
| 其他任何 `Exception` | `SandboxInternalException` | 意外错误 |
## 最佳实践
### 1. 始终以 `SandboxException` 作为基类捕获
```python
try:
sandbox = await Sandbox.create("ubuntu", connection_config=config)
except SandboxException as e:
# 所有 SDK 错误都可以在此捕获
log.error(f"Sandbox 错误 [{e.error.code}]: {e.error.message}", exc_info=e)
```
### 2. 使用 `request_id` 进行日志关联
当 `SandboxApiException` 发生时,`request_id` 与服务端日志中的 ID 匹配。将其包含在错误报告中:
```python
except SandboxApiException as e:
log.error(
"API 错误: code=%s status=%d request_id=%s",
e.error.code, e.status_code, e.request_id,
)
```
### 3. 区分客户端错误和服务端错误
```python
except SandboxApiException as e:
if e.status_code and e.status_code >= 400 and e.status_code < 500:
# 客户端错误 — 检查输入参数
log.warning("客户端错误: %s", e.error.message)
elif e.status_code and e.status_code >= 500:
# 服务端错误 — 重试或上报
log.error("服务端错误: %s", e.error.message)
```
### 4. 使用合适的策略处理池错误
```python
from opensandbox.exceptions import PoolEmptyException, PoolAcquireFailedException
try:
sandbox = pool.acquire(policy=AcquirePolicy.FAIL_FAST)
except PoolEmptyException:
# 没有空闲 sandbox — 回退到直接创建
sandbox = await Sandbox.create("ubuntu", connection_config=config)
except PoolAcquireFailedException:
# 空闲候选不可用 — 回退到直接创建
sandbox = await Sandbox.create("ubuntu", connection_config=config)
```
### 5. 对瞬时网络错误进行重试
```python
import asyncio
from opensandbox.exceptions import SandboxInternalException
async def create_with_retry(image, config, retries=3, delay=2):
for attempt in range(retries):
try:
return await Sandbox.create(image, connection_config=config)
except SandboxInternalException as e:
if attempt < retries - 1:
await asyncio.sleep(delay * (attempt + 1))
else:
raise
```
## SDK 错误码常量
SDK 定义了自己的 `SandboxError` 错误码常量(位于 `opensandbox.exceptions.SandboxError`),用于 SDK 内部产生的错误,而非来自服务端的错误:
| 常量 | 值 | 使用场景 |
|------|----|----------|
| `INTERNAL_UNKNOWN_ERROR` | `"INTERNAL_UNKNOWN_ERROR"` | `SandboxException` 和 `SandboxInternalException` 的默认值 |
| `READY_TIMEOUT` | `"READY_TIMEOUT"` | `SandboxReadyTimeoutException` |
| `UNHEALTHY` | `"UNHEALTHY"` | `SandboxUnhealthyException` |
| `INVALID_ARGUMENT` | `"INVALID_ARGUMENT"` | `InvalidArgumentException` |
| `UNEXPECTED_RESPONSE` | `"UNEXPECTED_RESPONSE"` | `SandboxApiException` 默认值 |
| `POOL_EMPTY` | `"POOL_EMPTY"` | `PoolEmptyException` |
| `POOL_ACQUIRE_FAILED` | `"POOL_ACQUIRE_FAILED"` | `PoolAcquireFailedException` |
| `POOL_STATE_STORE_UNAVAILABLE` | `"POOL_STATE_STORE_UNAVAILABLE"` | `PoolStateStoreUnavailableException` |
| `POOL_STATE_STORE_CONTENTION` | `"POOL_STATE_STORE_CONTENTION"` | `PoolStateStoreContentionException` |
| `POOL_NOT_RUNNING` | `"POOL_NOT_RUNNING"` | `PoolNotRunningException` |
当错误源自 SDK 时,这些常量出现在 `e.error.code` 中。服务端产生的错误使用 `NAMESPACE::CODE` 格式(如 `DOCKER::SANDBOX_NOT_FOUND`)。
::: tip
通过 `e.error.code` 的格式区分错误来源:SDK 错误码是简单字符串如 `READY_TIMEOUT`,服务端错误码遵循 `NAMESPACE::CODE` 模式如 `DOCKER::SANDBOX_NOT_FOUND`。
:::