# PR #47165 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[Bugfix] Return HTTP 422 for unprocessable image URLs instead of 500
- 合并时间：2026-07-06 19:56
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/47165

---

# 执行摘要

- 一句话：图片 URL 错误时返回 HTTP 422 而非 500
- 推荐动作：值得精读。该 PR 展示了如何正确区分客户端错误与服务器错误，以及如何在 Review 中根据反馈调整设计（尤其是瞬态错误的保留）。错误包装函数 `_wrap_media_fetch_error` 的设计模式（基于异常类型和状态码分类）在自定义错误处理时很有参考价值。同时，测试策略的调整（移除内部 helper 的直接测试以降低维护摩擦）也体现了务实的测试理念。

# 功能与动机

当用户提交无效图片 URL 时，当前返回 HTTP 500，误导用户以为是服务器端问题，导致调试困难。引用 Issue #47163：'Image URL errors return HTTP 500 instead of 422 for unprocessable content'。

# 实现拆解

### 实现拆解

1. **新增异常类**：在 `vllm/exceptions.py` 中添加 `VLLMUnprocessableEntityError`（继承 `ValueError`），支持 `parameter` 和 `value` 属性，用于携带导致错误的请求参数和值。

2. **错误包装函数**：在 `vllm/multimodal/media/connector.py` 中编写 `_wrap_media_fetch_error()`，识别 `aiohttp.ClientResponseError`、`requests.exceptions.HTTPError`、`requests.exceptions.InvalidURL`、`ValueError` 等异常类型，根据状态码决定是否转换为 `VLLMUnprocessableEntityError`：永久客户端错误（4xx 除 408/429、InvalidURL、ValueError）转换；瞬态错误（5xx、408/429、DNS、连接错误、超时）保持原样以支持重试。

3. **修改媒体获取入口**：在 `MediaConnector.load_from_url` 和 `load_from_url_async` 同步 / 异步两路中，将原本直接调用 `connection.get_bytes` 和 `async_get_bytes` 包裹在 try/except 中，出现异常时经过 `_wrap_media_fetch_error` 处理，若结果为 `VLLMUnprocessableEntityError` 则直接抛出。

4. **错误响应处理**：在 `vllm/entrypoints/serve/utils/error_response.py` 的 `create_error_response()` 中增加 `isinstance(exc, VLLMUnprocessableEntityError)` 分支，设置 `err_type` 为 `"UnprocessableEntityError"`，`status_code` 为 `HTTPStatus.UNPROCESSABLE_ENTITY`（422）。

5. **API 服务注册**：在 `vllm/entrypoints/openai/api_server.py` 中导入新异常并调用 `app.exception_handler(VLLMUnprocessableEntityError)(exception_handler)` 注册处理器。

6. **测试覆盖**：新增 `tests/multimodal/media/test_unprocessable_entity_error.py`，包含异常创建测试、MediaConnector 同步 / 异步错误处理测试（404→422、500→500 保留、DNS 错误保留等）。根据 review 反馈，移除了对内部 `_wrap_media_fetch_error` 的直接单元测试和集成测试类，以减少维护摩擦。

关键文件：
- `tests/multimodal/media/test_unprocessable_entity_error.py`（模块 多模态测试；类别 test；类型 test-coverage；符号 TestVLLMUnprocessableEntityError, test_creation, test_creation_with_parameter_and_value, test_is_value_error_subclass）: 新增完整测试套件，覆盖异常类创建、MediaConnector 同步 / 异步错误处理、瞬态错误保留等场景；经 review 精简后保留高价值测试
- `vllm/exceptions.py`（模块 异常；类别 source；类型 core-logic；符号 VLLMUnprocessableEntityError, __init__, __str__）: 新增 VLLMUnprocessableEntityError 异常类，作为 422 错误的载体，继承 ValueError 便于复用已有验证处理流程
- `vllm/multimodal/media/connector.py`（模块 多模态；类别 source；类型 dependency-wiring；符号 _wrap_media_fetch_error）: 实现核心错误包装函数 _wrap_media_fetch_error，区分永久客户端错误与瞬态错误，并修改 MediaConnector 的 load_from_url 和 load_from_url_async 以捕获异常并包装
- `vllm/entrypoints/serve/utils/error_response.py`（模块 前端入口；类别 source；类型 dependency-wiring）: 在 create_error_response 中添加对 VLLMUnprocessableEntityError 的处理分支，映射到 HTTP 422
- `vllm/entrypoints/openai/api_server.py`（模块 API 服务；类别 source；类型 entrypoint）: 注册异常处理器，使 VLLMUnprocessableEntityError 能被全局捕获

关键符号：VLLMUnprocessableEntityError.__init__, VLLMUnprocessableEntityError.__str__, _wrap_media_fetch_error, create_error_response, MediaConnector.load_from_url, MediaConnector.load_from_url_async

## 关键源码片段

### `vllm/exceptions.py`

新增 VLLMUnprocessableEntityError 异常类，作为 422 错误的载体，继承 ValueError 便于复用已有验证处理流程

```python
class VLLMUnprocessableEntityError(ValueError):
    """当请求内容无法处理时抛出的异常，例如不合法的图片 URL。"""

    def __init__(
        self,
        message: str,
        *,
        parameter: str | None = None,
        value: Any = None,
    ) -> None:
        super().__init__(message)
        self.parameter = parameter  # 导致错误的参数名称（如 "image_url"）
        self.value = value  # 导致错误的参数值

    def __str__(self):
        base = super().__str__()
        extras = []
        if self.parameter is not None:
            extras.append(f"parameter={self.parameter}")
        if self.value is not None:
            extras.append(f"value={self.value}")
        # 如果有额外信息，附加在消息末尾
        return f"{base} ({', '.join(extras)})" if extras else base

```

### `vllm/multimodal/media/connector.py`

实现核心错误包装函数 _wrap_media_fetch_error，区分永久客户端错误与瞬态错误，并修改 MediaConnector 的 load_from_url 和 load_from_url_async 以捕获异常并包装

```python
def _wrap_media_fetch_error(
    url: str, exc: Exception
) -> VLLMUnprocessableEntityError | Exception:
    # 根据异常类型和状态码区分永久客户端错误（422）与可重试的瞬态错误
    if isinstance(exc, aiohttp.ClientResponseError):
        # 408 和 429 是临时性的，不转换为 422
        if exc.status in (408, 429):
            return exc
        # 非 5xx 的客户端错误（如 400, 401, 403, 404 等）视为永久
        if exc.status < 500:
            return VLLMUnprocessableEntityError(
                f"Failed to fetch media from URL: HTTP {exc.status} error",
                parameter="image_url",
                value=url,
            )
        # 5xx 服务器错误保留原样
        return exc

    # 处理 requests 库的 HTTP 错误，逻辑同上
    if isinstance(exc, requests.exceptions.HTTPError):
        if exc.response is not None:
            status_code = exc.response.status_code
            if status_code in (408, 429):
                return exc
            if status_code < 500:
                return VLLMUnprocessableEntityError(
                    f"Failed to fetch media from URL: HTTP {status_code} error",
                    parameter="image_url",
                    value=url,
                )
        return exc

    # 无效 URL 格式转换为 422
    if isinstance(exc, requests.exceptions.InvalidURL):
        return VLLMUnprocessableEntityError(
            "Failed to fetch media from URL: Invalid URL format",
            parameter="image_url",
            value=url,
        )

    # 通用 ValueError（如 URL 解析失败）也视为 422
    if isinstance(exc, ValueError):
        return VLLMUnprocessableEntityError(
            "Failed to fetch media from URL: Invalid URL",
            parameter="image_url",
            value=url,
        )

    # 其他未分类异常（如网络瞬态错误）保持原样
    return exc

```

# 评论区精华

- **错误分类讨论**：用户 @hclsys 指出 DNS 错误、连接超时等也是瞬态错误，应保留为可重试而非 422。作者 @akinsella 接受并更新实现，将 408、429、`ClientConnectorDNSError`、`ClientConnectionError`、`ServerDisconnectedError`、`TimeoutError` 排除在 422 之外，仅对明确的不良 URL（非 408/429 的 4xx、InvalidURL、ValueError）返回 422。[ 已解决 ]
- **代码质量讨论**：维护者 @DarkLight1337 要求将 `aiohttp` 和 `requests` 的导入移至文件顶部，删除函数内重复注释，移除对内部 helper 的直接测试（`TestWrapMediaFetchError`、`TestIntegration`）以减少未来修改时的维护阻力。作者逐条落实。[ 已解决 ]
- **安全提醒**：自动化审查 `depthfirst-app[bot]` 指出异常消息可能嵌入原始网络错误信息（主机名、端口等），可能辅助 SSRF 侦察。PR 未对此做额外处理，风险待评估。[ 未解决 ]

 - 瞬态错误是否应转换为 422 (design): 作者更新实现，将 408、429、ClientConnectorDNSError、ClientConnectionError、ServerDisconnectedError、TimeoutError 排除，仅将明确的不良 URL 转换为 422。
- 导入位置和注释清理 (style): 作者照做。
- 移除对内部 helper 的直接测试 (testing): 作者移除相应测试类，保留集成测试。
- 异常消息可能泄露内部信息 (security): 未在 PR 中解决，可能后续改进。

# 风险与影响

- 风险：
 - **信息泄露（LOW）**：当 422 响应时，`VLLMUnprocessableEntityError` 的消息字符串直接包含由 `str(exc)` 产生的原始错误描述，可能暴露内网主机名、端口等细节，对 SSRF 攻击有辅助作用。建议对所有外部不可信的错误使用通用消息，仅在日志中保留细节。
 - **错误分类边界**：通过状态码 408/429 和异常类型区分永久 / 瞬态，在多数场景合理，但自定义异常扩展时需保持同步，否则可能出现误分类。
 - **测试覆盖风险**：虽移除了对内部 helper 的直接测试，但保留的集成测试仍覆盖了主要路径，风险较小。
 - **回归风险**：修改了媒体获取核心路径（`load_from_url`/`load_from_url_async`），影响所有通过 HTTP 加载图片的请求，需回归验证其他模态（音频、视频）是否受影响（本 PR 仅处理图片 URL，但错误包装函数在 Connector 级别生效，可能覆盖其他模态的请求）。
- 影响：
 - **用户影响**：当提交无效图片 URL 时，从 HTTP 500 变为 HTTP 422，使客户端能正确处理请求错误，改善调试体验。
 - **系统影响**：无性能影响，仅增加少量异常处理逻辑。
 - **团队影响**：建立了新的异常分类模式和错误包装函数，为后续其他参数验证提供了参考。
 - **影响范围**：影响 `/v1/chat/completions` 和 `/v1/responses` API 中涉及多模态图片 URL 的请求。
 - 风险标记：异常消息可能泄露网络细节 , 瞬态 / 永久错误分类边界需持续维护 , 间接影响其他模态（音频 / 视频）的错误处理

# 关联脉络

- 暂无明显关联 PR