Prhub

#47165 [Bugfix] Return HTTP 422 for unprocessable image URLs instead of 500

原始 PR 作者 akinsella 合并时间 2026-07-06 19:56 文件变更 5 提交数 6 评论 21 代码增减 +288 / -12

执行摘要

图片 URL 错误时返回 HTTP 422 而非 500

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

值得精读。该 PR 展示了如何正确区分客户端错误与服务器错误,以及如何在 Review 中根据反馈调整设计(尤其是瞬态错误的保留)。错误包装函数 _wrap_media_fetch_error 的设计模式(基于异常类型和状态码分类)在自定义错误处理时很有参考价值。同时,测试策略的调整(移除内部 helper 的直接测试以降低维护摩擦)也体现了务实的测试理念。

讨论亮点
  • 错误分类讨论:用户 @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 未对此做额外处理,风险待评估。[未解决]

实现拆解

实现拆解

  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 多模态测试 added 7.32
vllm/exceptions.py 异常 modified 7.74
vllm/multimodal/media/connector.py 多模态 modified 7.63
vllm/entrypoints/serve/utils/error_response.py 前端入口 modified 6.08
vllm/entrypoints/openai/api_server.py API 服务 modified 5.37

关键符号

VLLMUnprocessableEntityError.__init__ VLLMUnprocessableEntityError.__str__ _wrap_media_fetch_error create_error_response MediaConnector.load_from_url MediaConnector.load_from_url_async

关键源码片段

vllm/exceptions.py core-logic

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

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 dependency-wiring

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

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

评论区精华

瞬态错误是否应转换为 422 设计

用户 hclsys 指出 DNS 错误、连接错误等也是瞬态,应保留为可重试,不应映射到 422。

结论:作者更新实现,将 408、429、ClientConnectorDNSError、ClientConnectionError、ServerDisconnectedError、TimeoutError 排除,仅将明确的不良 URL 转换为 422。 · 已解决

导入位置和注释清理 style

维护者要求将 aiohttp 和 requests 导入移到文件顶部,删除不必要的注释。

结论:作者照做。 · 已解决

移除对内部 helper 的直接测试 测试

维护者认为对 _wrap_media_fetch_error 的直接测试会因实现细节变更而产生维护摩擦,建议移除 TestWrapMediaFetchError 和 TestIntegration 类。

结论:作者移除相应测试类,保留集成测试。 · 已解决

异常消息可能泄露内部信息 安全

自动化审查指出 str(exc) 可能暴露内部主机名、端口等,建议使用通用消息。

结论:未在 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 的请求。
异常消息可能泄露网络细节 瞬态 / 永久错误分类边界需持续维护 间接影响其他模态(音频 / 视频)的错误处理

关联 Issue

#47163 [Bug]: Image URL errors return HTTP 500 instead of 422 for unprocessable content

完整报告

参与讨论