Prhub

#33894 refactor error responses into shared utils::response helpers

原始 PR 作者 rainj-me 合并时间 2026-08-13 16:30 文件变更 10 提交数 2 评论 1 代码增减 +187 / -159

执行摘要

Rust API 错误响应抽成共享 helper,原生与 OpenAI 路径统一

PR body 为空模板,动机来自提交信息与代码注释:'Unify the native and OpenAI error paths on one error_response / error_value / sse_error_response set in utils::response'。此前错误响应的塑造逻辑分散在 frame.rs(error_value)、submit.rs(pre_submit_error + sse_error_response)与 openai.rs(openai_error_response + streaming_error)三处,各自实现同一套 unary→JSON、streaming→200 + SSE 的规则,新增 endpoint 时容易走样。重构把'wire shape(协议自有)'与'shaping 逻辑(通用)'分开:JSON body 形状仍由各 endpoint 拥有,响应塑造统一下沉到共享模块。

值得快速精读。核心价值不在功能而在边界划分:payload 形状(协议自有)与响应塑造(通用逻辑)分离,以及用薄包装避免调用点重复参数。对要在 Rust 前端新增 endpoint 或修改错误契约的开发者是必读参考。sherlockwu 的唯一一条评论直接决定了最终 API 形态,是'共享 helper + 薄包装'的经典落地案例。

讨论亮点

唯一一条 review 评论来自 sherlockwu(合入者),针对初始实现中每个调用点要写两次状态码的问题:

Can we have a thin wrapper (one native, one OpenAI) so the status code don't need to be typed twice per call site? like fn native_error(code, message, stream) { error_response(code, error_value(code.as_u16(), message), stream) }

评论直接塑造了最终 API 形态:head 版本中 native_api::native_erroropenai::openai_error 均按建议实现为薄包装,调用点只需传一次状态码,body 拼装与 unary / streaming 分支全部下沉。该线程已解决,作者在第二个提交 'address comment' 中落实。

实现拆解

  1. 新增共享模块 utils::response:新建 rust/sglang-server/src/utils/response.rs(+94 行),定义 error_value(原生 {"error": {message, code}} body)、error_response(unary → 状态码 + JSON,streaming → 200 + SSE 错误帧 + [DONE])与 sse_error_response(单错误帧 + [DONE]),并在 utils.rs 注册 pub mod response。模块内附 error_responses_match_python_shape 测试,把 Python parity 契约(unary 形态与 streaming 形态)固定下来。
  2. 原生路径迁移frame.rs 删除私有 error_valuesubmit.rs 删除 pre_submit_errorsse_error_response 及其测试(净删 80 行),TM 通道关闭时的 503 改调 native_api::native_errornative_api.rs 新增薄包装 native_error(code, message, stream)generate 入口的 JSON 解析失败、batch 校验失败、prefetch 失败等错误路径全部改调它。
  3. OpenAI 路径迁移openai.rserror_payload 改为 pub(super) 且 message 泛化为 impl Into<String>;删除 openai_error_responsestreaming_erroropenai_error 新增 stream: bool 参数并委托共享的 error_responsecompletions.rs / chat.rs / models.rs 的所有 pre-submit 调用点显式传 false;流内错误帧(truncated、EgressItem::Error、abort_status)由 streaming_error 改为直接构造 error_payload 并转字符串,StatusCode::from_u16(...).unwrap_or(500) 回退逻辑保持原样。
  4. 测试与配套test_utils.rsopenai_error_response_covers_unary_and_sse 改为消费新 openai_error 签名并更新注释;response.rs 自带的 parity 测试替代原 submit.rs 中的 pre_submit_errors_match_python_shape;PR 已打 run-ci 标签。
文件 模块 状态 重要度
rust/sglang-server/src/utils/response.rs 错误响应 added 8.56
rust/sglang-server/src/api_server/openai.rs OpenAI 网关 modified 7.44
rust/sglang-server/src/api_server/submit.rs 请求提交 modified 7.37
rust/sglang-server/src/api_server/native_api.rs 原生接口 modified 5.94
rust/sglang-server/src/api_server/openai/completions.rs 补全接口 modified 6.42
rust/sglang-server/src/api_server/openai/chat.rs 对话接口 modified 5.95
rust/sglang-server/src/api_server/frame.rs 帧封装 modified 5.0
rust/sglang-server/src/api_server/openai/test_utils.rs 测试工具 modified 4.48
rust/sglang-server/src/api_server/openai/models.rs 模型接口 modified 4.19
rust/sglang-server/src/utils.rs 工具模块 modified 3.98

关键符号

error_value error_response sse_error_response native_error openai_error error_payload

关键源码片段

rust/sglang-server/src/utils/response.rs core-logic

本 PR 的核心新增文件,统一承载原生与 OpenAI 共用的错误响应塑造逻辑(unary → JSON,streaming → 200 + SSE 错误帧 + [DONE]),并内置 Python parity 测试。

//! utils/response.rs —— 共享的 HTTP 错误响应塑造逻辑。
//!
//! 两类东西刻意分开:WIRE SHAPES(JSON body 形状)仍归各 endpoint 所有,
//! 这里只负责「怎么把错误变成 HTTP 响应」。原生 API 用 `error_value`
//! 构造 `{"error": {message, code}}`,OpenAI 前端用自己的 `error_payload`,
//! 两者都经由 `error_response` 出网;PD bootstrap registry 的纯文本 body
//! 是协议自有的,刻意不在此统一。use std::convert::Infallible;use axum::{
    Json,
    http::StatusCode,
    response::{IntoResponse, Response, sse::{Event, Sse}},
};/// 原生错误 body:统一的 `{"error": {...}}` 对象,不是裸文本 ——
/// 裸文本会让按 JSON 解析的客户端直接报错。
pub fn error_value(code: u16, message: &str) -> serde_json::Value {
    serde_json::json!({ "error": { "message": message, "code": code } })
}/// 按客户端已承诺的形态出错误:
/// unary → 状态码 + JSON body;streaming → 200 + 一个 SSE 错误帧 + `[DONE]`,
/// 因为客户端已经在读流(Python 端也在 `stream_results()` 里流内应答)。
/// body 由调用者决定形状:原生 `error_value` 或 OpenAI `error_payload`。
pub fn error_response(code: StatusCode, body: serde_json::Value, stream: bool) -> Response {
    if !stream {
        return (code, Json(body)).into_response();
    }
    sse_error_response(body)
}/// 200 SSE 响应:一个错误帧 + `[DONE]`。
/// 原生 API 与 OpenAI 前端共用;状态码折进 body(响应状态已是 200)。
pub fn sse_error_response(body: serde_json::Value) -> Response {
    let frames = [body.to_string(), "[DONE]".to_string()];
    Sse::new(futures::stream::iter(
        frames.map(|data| Ok::<_, Infallible>(Event::default().data(data))),
    ))
    .into_response()
}#[cfg(test)]
mod tests {
    use super::*;    /// Python-parity 钉子(对齐 `http_server.generate_request`):
    /// unary 错误是 4xx/5xx + JSON `{"error": ...}`;
    /// streaming 错误是 200 + 一个 SSE 错误帧 + `[DONE]`。
    #[tokio::test]
    async fn error_responses_match_python_shape() {
        // unary 分支:状态码透传,body 为 JSON 对象
        let unary = error_response(StatusCode::BAD_REQUEST, error_value(400, "bad input"), false);
        assert_eq!(unary.status(), StatusCode::BAD_REQUEST);
        let body = axum::body::to_bytes(unary.into_body(), 64 * 1024).await.unwrap();
        let v: serde_json::Value = serde_json::from_slice(&body).expect("JSON body");
        assert_eq!(v["error"]["message"], "bad input");
        assert_eq!(v["error"]["code"], 400);        // streaming 分支:HTTP 本身是 200,状态码以 "code" 字段内嵌
        let streamed = error_response(StatusCode::BAD_REQUEST, error_value(400, "bad input"), true);
        assert_eq!(streamed.status(), StatusCode::OK, "the stream itself is 200");
        let body = axum::body::to_bytes(streamed.into_body(), 64 * 1024).await.unwrap();
        let text = String::from_utf8(body.to_vec()).unwrap();
        assert!(text.contains(r#""code":400"#), "carries the status in-band: {text}");
        assert!(text.trim_end().ends_with("data: [DONE]"), "terminated: {text}");
    }
}
rust/sglang-server/src/api_server/openai.rs entrypoint

OpenAI 错误路径的收口点:error_payload 公开化并泛化参数,openai_error 扩展 stream 参数后委托共享 error_response,删除 openai_error_response 与 streaming_error。

// api_server/openai.rs —— OpenAI 前端的错误 body 与薄包装。
// `error_payload` 是协议自有的 body 形状;`openai_error` 只负责把
// 状态码、body、stream 标志一次性交给共享的 `utils::response::error_response`。/// OpenAI 错误 payload:`type` 是 SDK 面对的错误类别
/// (`AuthenticationError` / `InternalServerError` / `BadRequestError`),
/// `code` 携带 HTTP 状态 —— 对齐 Python OpenAI 前端的输出形状。
pub(super) fn error_payload(code: StatusCode, message: impl Into<String>) -> serde_json::Value {
    let message = message.into();
    let error_type = if code == StatusCode::UNAUTHORIZED {
        "AuthenticationError"
    } else if code.is_server_error() {
        "InternalServerError"
    } else {
        "BadRequestError"
    };
    serde_json::json!({
        "error": {
            "object": "error",
            "message": message,
            "type": error_type,
            "param": null,
            "code": code.as_u16(),
        }
    })
}/// OpenAI 错误响应薄包装:调用点只需写一次状态码,
/// body 拼装与 unary / streaming 分支全部下沉到共享的 `error_response`。
pub(super) fn openai_error(code: StatusCode, message: impl Into<String>, stream: bool) -> Response {
    error_response(code, error_payload(code, message), stream)
}// api_server/native_api.rs 中对应的原生侧薄包装,语义完全对称:
// body 用原生 `error_value`,其余逻辑同样委托 `error_response`。
// pub(super) fn native_error(code: StatusCode, message: &str, stream: bool) -> Response {
// error_response(code, error_value(code.as_u16(), message), stream)
// }

评论区精华

薄包装函数避免调用点重复传状态码 设计

sherlockwu 在 chat.rs 的 diff hunk 上建议:与其在每个调用点写两次状态码(error_response 与 error_payload 各一次),不如提供 native_error 与 openai_error 两个薄包装,内部拼装 body 并处理 stream 分支,并直接给出了示例代码。

结论:已采纳:native_api.rs 新增 native_error,openai.rs 的 openai_error 扩展为带 stream 参数的薄包装,所有调用点只需传一次状态码。 · 已解决

风险与影响

回归风险(行为不变性):openai_error 从'恒 unary'改为显式 stream 参数后,completions.rs / chat.rs / models.rs 共数十处调用点手工补 false,任何遗漏或误传都会改变错误响应形态(unary 4xx ↔ 200 + SSE);当前调用点均为 pre-submit 校验场景,与旧语义一致,测试覆盖了 unary 与 SSE 双分支。结构性风险:streaming_error 删除后,其 StatusCode::from_u16 + unwrap_or(500) 逻辑内联进 completions.rschat.rs 两个 streaming 循环,后续调整流内错误帧格式需改两处。模块耦合:submit.rs 依赖 native_api::native_error,而 native_api 又依赖 submit::submit,Rust 允许模块级循环但仍增加了耦合。兼容性:对客户端无 wire 变化,error_payload 泛化为 impl Into<String> 属纯内部签名变化,message.into() 提前物化保证 &str / String 调用点均安全。

对用户 / API 客户端无可见行为变化,错误响应形状与 Python 侧保持对齐,是'零行为变更'的重构。对系统:Rust API server 的错误路径收敛到单一模块,新增 endpoint 只需选 body 构造器(原生 error_value 或 OpenAI error_payload)再调 error_response,错误契约在 response.rs 模块注释中集中文档化。对团队:跨 10 个文件的机械替换,review 焦点集中在 response.rs 与两个薄包装;未来若演进错误格式(如统一 error object 字段)只需改一处。影响面覆盖所有原生 /generate/health_generate 与 OpenAI chat / completions / models 端点的错误响应路径。

调用点批量签名变更 流式错误构造内联 行为不变依赖测试兜底

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论