Prhub

#45286 [Bugfix][Rust Frontend] Return 400 for prompt-validation submit errors

原始 PR 作者 xiaguan 合并时间 2026-06-12 15:53 文件变更 4 提交数 1 评论 3 代码增减 +89 / -18

执行摘要

Rust 前端提示验证失败返回 400 而非 500

PR 描述指出,prompt 验证失败(如 PromptTooLongEmptyPromptTokenIds)在三次提交站点中被 blanket server_error! 包裹,客户端发送超过 max_model_len 的 prompt 时得到 HTTP 500 server_error,而非 Python 前端返回的 400 invalid_request_error。客户端无法区分“请求无效”和“服务器故障”,导致重试逻辑错误地重复提交不可能成功的请求。

值得精读,尤其是错误分类模式 text_submit_error/chat_submit_error 的设计。展示了如何通过类型匹配实现分层错误处理,并且包含了完整的测试覆盖。

讨论亮点

Codex 审查指出,is_prompt_validation_error 最初只匹配顶层 vllm_text::Error::EmptyPromptTokenIds,忽略了通过 Llm 包装的 EmptyPromptTokenIds(在 prepare 阶段出现)。作者响应并修复,添加了 vllm_text::Error::Llm(vllm_llm::Error::EmptyPromptTokenIds) 分支及对应测试。最终审核者 BugenZhao 批准。

实现拆解

  1. 新增错误分类函数:在 rust/src/server/src/error.rs 中定义 text_submit_errorchat_submit_error,以及辅助函数 is_prompt_validation_errortext_submit_error 检查 vllm_text::Error 是否为 PromptTooLongEmptyPromptTokenIds 或通过 Llm 包装的 EmptyPromptTokenIds,是则返回 ApiError::InvalidRequest(400),否则返回 ApiError::ServerError(500)。chat_submit_error 处理 vllm_chat::Error,同样分类其持有的 PromptTooLong 及包裹的文本验证错误。
  2. 替换入口点中的调用:在三个路由文件 completions.rschat_completions.rsinference/generate.rs 中,将原先直接构造 server_error! 的代码替换为调用对应的分类函数,传入上下文描述字符串和错误对象。
  3. 添加单元测试:在 error.rs#[cfg(test)] mod tests 中增加 prompt_too_long_maps_to_invalid_requestchat_wrapped_prompt_too_long_maps_to_invalid_requestllm_wrapped_empty_prompt_maps_to_invalid_requestother_submit_errors_stay_internal 四个测试用例,覆盖关键路径。
文件 模块 状态 重要度
rust/src/server/src/error.rs 错误处理 modified 8.46
rust/src/server/src/routes/openai/completions.rs 请求路由 modified 5.27
rust/src/server/src/routes/openai/chat_completions.rs 请求路由 modified 5.27
rust/src/server/src/routes/inference/generate.rs 请求路由 modified 5.31

关键符号

text_submit_error chat_submit_error is_prompt_validation_error

关键源码片段

rust/src/server/src/error.rs core-logic

核心变更文件,新增错误分类函数和完整单元测试,是本次 PR 的逻辑主体。

// use thiserror_ext::AsReport as _; // 新导入,用于获取错误的可读字符串表示/// Classify a text-pipeline submit failure: tokenized-prompt validation
/// failures (the prompt is too long for the model, or empty after
/// tokenization) are the client's fault and map to HTTP 400, mirroring the
/// Python frontend. Everything else stays an internal 500.
pub fn text_submit_error(context: &'static str, error: vllm_text::Error) -> ApiError {
    if is_prompt_validation_error(&error) {
        return invalid_request!("{error}");
    }
    server_error!("{}: {}", context, error.to_report_string())
}/// Like [`text_submit_error`], for the chat pipeline (which both wraps the
/// text errors and raises its own prompt-length variant).
pub fn chat_submit_error(context: &'static str, error: vllm_chat::Error) -> ApiError {
    match &error {
        // chat 层直接抛出的 PromptTooLong
        vllm_chat::Error::PromptTooLong { .. } => invalid_request!("{error}"),
        // chat 层包裹的文本错误,若为验证错误也返回 400
        vllm_chat::Error::Text(text_error) if is_prompt_validation_error(text_error) => {
            invalid_request!("{error}")
        }
        _ => server_error!("{}: {}", context, error.to_report_string()),
    }
}/// Returns true if the text error indicates a prompt validation failure that
/// should be the client's responsibility.
fn is_prompt_validation_error(error: &vllm_text::Error) -> bool {
    matches!(
        error,
        vllm_text::Error::PromptTooLong { .. }
            | vllm_text::Error::EmptyPromptTokenIds { .. }
            // An empty tokenized prompt detected later, at request prepare
            // time, surfaces through the transparent Llm wrapper.
            | vllm_text::Error::Llm(vllm_llm::Error::EmptyPromptTokenIds { .. })
    )
}#[cfg(test)]
mod tests {
    use super::*;    #[test]
    fn prompt_too_long_maps_to_invalid_request() {
        // 裸的 PromptTooLong 应映射到 400 并包含 token 数量信息
        let error = vllm_text::Error::PromptTooLong {
            max_model_len: 8192,
            prompt_len: 9000,
        };
        let api_error = text_submit_error("failed to submit completion request", error);
        assert_eq!(api_error.status_code(), StatusCode::BAD_REQUEST);
        let response = api_error.to_error_response();
        assert_eq!(response.error.error_type, "invalid_request_error");
        assert!(response.error.message.contains("8192"));
        assert!(response.error.message.contains("9000"));
    }    #[test]
    fn chat_wrapped_prompt_too_long_maps_to_invalid_request() {
        // chat 层包裹的 PromptTooLong 也应返回 400
        let error = vllm_chat::Error::Text(vllm_text::Error::PromptTooLong {
            max_model_len: 8192,
            prompt_len: 9000,
        });
        let api_error = chat_submit_error("failed to submit chat request", error);
        assert_eq!(api_error.status_code(), StatusCode::BAD_REQUEST);
    }    #[test]
    fn llm_wrapped_empty_prompt_maps_to_invalid_request() {
        // 通过 Llm 包装的 EmptyPromptTokenIds(在 prepare 阶段出现)也应返回 400
        let error = vllm_text::Error::Llm(vllm_llm::Error::EmptyPromptTokenIds {
            request_id: "req-1".to_string(),
        });
        let api_error = text_submit_error("failed to submit completion request", error);
        assert_eq!(api_error.status_code(), StatusCode::BAD_REQUEST);
    }    #[test]
    fn other_submit_errors_stay_internal() {
        // 非验证错误(如 tokenizer 失败)仍返回 500
        let error = vllm_text::Error::Tokenizer { message: "broken model".to_string() };
        let api_error = text_submit_error("failed to submit completion request", error);
        assert_eq!(api_error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
    }
}

评论区精华

处理嵌套的 EmptyPromptTokenIds 错误 正确性

Codex 审查指出 `is_prompt_validation_error` 最初只匹配顶层 `vllm_text::Error::EmptyPromptTokenIds`,忽略了通过 `Llm` 包装的 `EmptyPromptTokenIds`。

结论:作者添加了 `vllm_text::Error::Llm(vllm_llm::Error::EmptyPromptTokenIds)` 分支及对应测试。 · 已解决

风险与影响

本次变更仅影响错误分类逻辑,不涉及核心推理路径。风险较低。可能的隐患:chat_submit_error 的模式匹配如果漏掉 vllm_chat::Error 下新增加的验证变体,会回退到 500,但已有测试覆盖现有变体。另外,text_submit_error 对非验证错误保留 500,行为不变。总体风险可控。

对客户端:之前收到 HTTP 500 的 prompt 错误现在收到 HTTP 400,重试行为更合理。对系统:与 Python 前端行为对齐,提升 API 一致性。影响范围仅限于 Rust 前端的三个提交端点。对团队:无负面影响。

状态码变更 入口点修改 模式匹配覆盖面

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论