From 23aea890189f2ee1b4c049233a27f9338b98bfb2 Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 21 Sep 2026 20:16:59 +0800 Subject: [PATCH] fix(gateway): give the request body limit one source and raise it to 277 MiB The limit lives on two layers: axum's extractor (`DefaultBodyLimit::max`, the only writer of the `DefaultBodyLimitKind` extension that overrides the 2 MiB `DEFAULT_LIMIT`) and tower-http's outer guard (`RequestBodyLimitLayer::new`, which answers 413 without reading the body when `Content-Length` exceeds its own number). The outer layer cannot raise the inner one, but it wins whenever it is smaller. Raising the constant from 8 MiB to 277 MiB while the outer layer still carried `70 * 1024 * 1024` would have capped the effective limit at 70 MB - measured, a 71 MiB body came back `413 length limit exceeded` (tower-http's own body text, not axum's `Failed to buffer the request body: ...`). - `GATEWAY_BODY_LIMIT` 8 MiB -> 277 MiB, and the outer layer now references that same constant instead of a literal of its own - move the outer layer into `routes::router()` (it was built in `main.rs` with its own number) so the stack the tests exercise is the production stack - new gate `src/body_limit_gate.rs`: every layer constructor must take `GATEWAY_BODY_LIMIT`, and no second `...BODY_LIMIT` constant may exist - probe sizes: 3 MiB -> 9 MiB (crosses the old 8 MiB) plus a new 71 MiB leg that fails on the pre-fix outer layer; the login negative control is unchanged Tests: 302 -> 308 passed, fmt/clippy clean. --- src/body_limit_gate.rs | 227 +++++++++++++++++++++++++++++++++++++++++ src/gateway.rs | 93 +++++++++++++++-- src/main.rs | 13 ++- src/routes/mod.rs | 39 +++++-- 4 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 src/body_limit_gate.rs diff --git a/src/body_limit_gate.rs b/src/body_limit_gate.rs new file mode 100644 index 0000000..75e6c75 --- /dev/null +++ b/src/body_limit_gate.rs @@ -0,0 +1,227 @@ +//! 请求体上限门禁(2026-09-21):**全树只有一个请求体上限的数**。 +//! +//! 起因(rant 2026-09-18T09:14:18 的第二次现身):上限分布在**两层**上 —— +//! axum 提取器那层(`DefaultBodyLimit::max`,`String` / `Json` 的 2 MiB 默认值只认它写的 +//! `DefaultBodyLimitKind` 扩展)与 tower-http 的外层粗闸(`RequestBodyLimitLayer::new`, +//! 只看 `Content-Length`,超限时**不读体直接 413**)。两层是**两个数**: +//! +//! - 外层写得更小 ⇒ 它**抢答**,实际生效的就是外层(2026-09-21 把内层从 8 MiB 抬到 +//! 277 MiB 时,外层还停在 `70 * 1024 * 1024`:实测 71 MiB 的体在外层被 413 掉 —— +//! 抬了个寂寞。这与 `v0.7.10` 那次「配了一个值 ≠ 生效了」是同族缺陷); +//! - 外层写得更大 ⇒ 它抬不动内层(不写那个扩展),实际生效的是内层。 +//! +//! **只有让两层引用同一个常量**,这个「min(两层) 才是真相」的陷阱才消失。本门禁把这条 +//! 不变量固化进 `cargo test`:任何一处层构造写了**别的数**、或另立**第二个** `…BODY_LIMIT` +//! 常量,都会在这里变红。 +//! +//! 设计约束(与 `i18n_pack.rs` / `perf_gate.rs` / `state_gate.rs` 同型): +//! - **仅测试期编译**(`#[cfg(test)] mod body_limit_gate`,见 `main.rs`),不进生产二进制; +//! - **零新依赖**:逐字节扫描 + `std::fs` 走一遍 `src/`,不解析 Rust; +//! - **生产区**=文件里**最后一个**行首 `#[cfg(test)]` 之前的部分(`main.rs` 的 +//! `mod xxx_gate;` 声明自身也是 `#[cfg(test)]` 顶格,故取**最后**一个而不是第一个); +//! - **跳过** `*_gate.rs` 与 `i18n_pack.rs`:这些模块把别的源码当**语料**内嵌(字符串里 +//! 就会出现同名调用),扫它们等于自救式误报; +//! - **阳性对照**:断言「走到的文件数 ≥ 20」「调用点处数恰为 1 / 3」「生产区确实被切短」, +//! 检测器另有三条合成输入的阴性 / 阳性对照(防「在空集上通过」,C2005 坑 68)。 +//! +//! ⚠️ **已知射程**:行注释按 `//` 截断(字符串字面量里的 `//` 也会被截,如测试里的 +//! `http://127.0.0.1:9`)—— 截断只会让那一行**少**看到内容;层构造行不会与 URL 同行, +//! 故只可能漏报、不可能误报。块注释 `/* */` 内的调用同样会被当成代码(今日 0 处)。 + +use std::path::PathBuf; + +/// 唯一真源的名字。 +const LIMIT: &str = "GATEWAY_BODY_LIMIT"; + +/// 待扫的层构造函数(含左括号)。 +const LAYERS: &[&str] = &["RequestBodyLimitLayer::new(", "DefaultBodyLimit::max("]; + +/// 生产区 = **最后一个**行首 `#[cfg(test)]` 之前的部分。 +fn production_region(src: &str) -> &str { + match src.rfind("\n#[cfg(test)]") { + Some(i) => &src[..i], + None => src, + } +} + +/// 按 `//` 截断行注释(保持行结构,便于报行号)。 +fn strip_line_comments(src: &str) -> String { + src.lines() + .map(|l| match l.find("//") { + Some(i) => &l[..i], + None => l, + }) + .collect::>() + .join("\n") +} + +/// 一处的实参文本(从左括号到配对右括号,跨行通过;去掉首尾空白与尾逗号)。 +fn call_sites(src: &str, callee: &str) -> Vec<(usize, String)> { + let text = strip_line_comments(src); + let mut out = Vec::new(); + let mut from = 0usize; + while let Some(rel) = text[from..].find(callee) { + let at = from + rel; + let line = text[..at].matches('\n').count() + 1; + let rest = &text[at + callee.len()..]; + let mut depth = 1i32; + let mut arg = String::new(); + for ch in rest.chars().take(400) { + match ch { + '(' => { + depth += 1; + arg.push(ch); + } + ')' => { + depth -= 1; + if depth == 0 { + break; + } + arg.push(ch); + } + _ => arg.push(ch), + } + } + out.push((line, arg.trim().trim_end_matches(',').trim().to_string())); + from = at + callee.len(); + } + out +} + +/// 层构造写了别的数(返回可读的违规描述)。 +fn violations(src: &str) -> Vec { + let mut out = Vec::new(); + for callee in LAYERS { + for (line, arg) in call_sites(src, callee) { + if arg != LIMIT { + out.push(format!("{line}: {callee}{arg}) —— 上限必须写成 {LIMIT}")); + } + } + } + out +} + +/// 生产区里所有以 `BODY_LIMIT` 结尾的标识符(应恒为 `GATEWAY_BODY_LIMIT`)。 +fn limit_names(src: &str) -> Vec { + let text = strip_line_comments(production_region(src)); + let mut out = Vec::new(); + for token in text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_')) { + if token.ends_with("BODY_LIMIT") && !token.is_empty() { + out.push(token.to_string()); + } + } + out.sort(); + out.dedup(); + out +} + +/// 走一遍 `src/`,返回 `(相对路径, 文本)`;跳过门禁模块自身。 +fn source_files() -> Vec<(String, String)> { + fn walk(dir: &PathBuf, root: &PathBuf, out: &mut Vec<(String, String)>) { + let Ok(rd) = std::fs::read_dir(dir) else { + return; + }; + for e in rd.flatten() { + let p = e.path(); + if p.is_dir() { + walk(&p, root, out); + } else if p.extension().map(|x| x == "rs").unwrap_or(false) { + let name = p.file_name().unwrap().to_string_lossy().to_string(); + // 门禁模块把别的源码当语料内嵌 ⇒ 扫它们会误报(见模块文档)。 + if name.ends_with("_gate.rs") || name == "i18n_pack.rs" { + continue; + } + let rel = p + .strip_prefix(root) + .unwrap_or(&p) + .to_string_lossy() + .replace('\\', "/"); + if let Ok(text) = std::fs::read_to_string(&p) { + out.push((rel, text)); + } + } + } + } + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut out = Vec::new(); + walk(&root, &root, &mut out); + out.sort(); + out +} + +#[test] +fn every_body_limit_layer_is_the_same_single_number() { + let files = source_files(); + assert!( + files.len() >= 20, + "只走到 {} 个源文件 —— 扫描器本身坏了(空集上通过是假绿)", + files.len() + ); + + let mut inner = 0usize; // `DefaultBodyLimit::max(…)` 处数 + let mut outer = 0usize; // `RequestBodyLimitLayer::new(…)` 处数 + let mut cut = 0usize; // 真的被切掉测试模块的文件数 + let mut bad: Vec = Vec::new(); + for (rel, src) in &files { + let prod = production_region(src); + if prod.len() < src.len() { + cut += 1; + } + for v in violations(src) { + bad.push(format!("{rel}:{v}")); + } + inner += call_sites(prod, LAYERS[1]).len(); + outer += call_sites(prod, LAYERS[0]).len(); + for name in limit_names(src) { + if name != LIMIT { + bad.push(format!("{rel}: 出现了第二个上限常量 `{name}`")); + } + } + } + assert!( + bad.is_empty(), + "请求体上限必须只有一个数({LIMIT}):\n{}", + bad.join("\n") + ); + // 阳性对照:调用点确实被扫到了(删掉一层 / 漏扫文件都会让计数掉下来), + // 且生产区切分确实生效(切割规则失效时,测试里的 `70 * 1024 * 1024` 会混进来)。 + assert!( + cut >= 5, + "只有 {cut} 个文件切出了生产区 —— 切割规则坏了(本仓带测试模块的文件远多于 5 个)" + ); + assert_eq!( + outer, 1, + "外层的 `RequestBodyLimitLayer::new(…)` 应恰有 1 处" + ); + assert_eq!(inner, 3, "三条网关路由各一处 `DefaultBodyLimit::max(…)`"); +} + +#[test] +fn the_layer_argument_is_read_across_lines() { + // rustfmt 会把长实参折行;实参抽取必须跨行(否则会读出空串而误判)。 + let folded = "fn f() {\n .layer(DefaultBodyLimit::max(\n GATEWAY_BODY_LIMIT,\n ))\n}\n#[cfg(test)]\nmod t {}\n"; + assert_eq!(call_sites(folded, LAYERS[1]), vec![(2, LIMIT.to_string())]); + assert!(violations(folded).is_empty()); +} + +#[test] +fn detector_flags_a_hardcoded_number() { + let src = "fn f() {\n .layer(RequestBodyLimitLayer::new(70 * 1024 * 1024))\n}\n#[cfg(test)]\nmod t {}\n"; + let v = violations(src); + assert_eq!(v.len(), 1, "硬编码的数必须被抓到:{v:?}"); + assert!(v[0].contains("70 * 1024 * 1024"), "{v:?}"); +} + +#[test] +fn detector_flags_a_second_constant_name() { + let src = "const INNER_BODY_LIMIT: usize = 8 * 1024 * 1024;\nfn f() {\n .layer(DefaultBodyLimit::max(INNER_BODY_LIMIT))\n}\n"; + assert_eq!(violations(src).len(), 1, "换了名字也是第二个数"); + assert_eq!(limit_names(src), vec!["INNER_BODY_LIMIT".to_string()]); +} + +#[test] +fn detector_ignores_commented_out_layers() { + let src = + "fn f() {\n // .layer(RequestBodyLimitLayer::new(70 * 1024 * 1024)) 曾经这样写\n}\n"; + assert!(violations(src).is_empty(), "{:?}", violations(src)); +} diff --git a/src/gateway.rs b/src/gateway.rs index 6c08f88..4a2fee9 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -1104,6 +1104,38 @@ mod tests { (status, bytes) } + /// 同 `post_raw`,但**显式带上 `content-length`**。 + /// + /// 真实客户端(curl / fetch / 各家 SDK)都会带这个头,而外层的 + /// `RequestBodyLimitLayer` **只看这个头**就决定是否抢答 413 ⇒ 要测外层就必须带上它。 + async fn post_raw_len( + st: AppState, + uri: &str, + body: String, + bearer: Option<&str>, + ) -> (StatusCode, Vec) { + let len = body.len(); + let mut b = Request::builder() + .method("POST") + .uri(uri) + .header("content-type", "application/json") + .header("content-length", len); + if let Some(k) = bearer { + b = b.header("authorization", format!("Bearer {k}")); + } + let resp = router() + .with_state(st) + .oneshot(b.body(Body::from(body)).unwrap()) + .await + .unwrap(); + let status = resp.status(); + let bytes = axum::body::to_bytes(resp.into_body(), 8 * 1024 * 1024) + .await + .unwrap() + .to_vec(); + (status, bytes) + } + async fn login_key(st: AppState) -> String { let (_, body) = post_raw( st, @@ -1116,13 +1148,22 @@ mod tests { v["api_key"].as_str().unwrap().to_string() } - /// 请求体上限(rant 2026-09-18T09:14:18):三条网关路由必须收得下 > 2 MiB 的请求体。 + /// 请求体上限(rant 2026-09-18T09:14:18):三条网关路由必须收得下远超默认值的请求体。 /// - /// 改前:`axum-core` 的 `DEFAULT_LIMIT = 2_097_152` 生效 —— `main.rs:239` 那层 - /// `RequestBodyLimitLayer(70MB)` 抬不动它(它不写 `DefaultBodyLimitKind` 扩展), - /// 3 MiB 的体在提取器里就被拒成 413;改后:per-route `DefaultBodyLimit::max` 抬到 8 MiB。 + /// 改前(v0.7.25 及以前):`axum-core` 的 `DEFAULT_LIMIT = 2_097_152` 生效 —— + /// tower-http 那层 `RequestBodyLimitLayer` 抬不动它(它不写 `DefaultBodyLimitKind` + /// 扩展)⇒ 3 MiB 的体在提取器里就被拒成 413。 + /// 改后:per-route `DefaultBodyLimit::max(GATEWAY_BODY_LIMIT)`;2026-09-21 由 8 MiB + /// 抬到 277 MiB ⇒ 探针取 **9 MiB**(跨过旧的那个 8 MiB,故它是这次抬升的回归腿)。 #[tokio::test] async fn gateway_routes_accept_bodies_past_the_default_limit() { + const PROBE: usize = 9 * 1024 * 1024; + const { + assert!( + PROBE > 8 * 1024 * 1024, + "探针必须大于旧上限 8 MiB,否则这条腿退化成只钉 axum 的 2 MiB 默认值" + ) + }; for (tag, uri) in [ ("bl_chat", "/v1/chat/completions"), ("bl_anth", "/anthropic/v1/messages"), @@ -1130,14 +1171,15 @@ mod tests { ] { let st = test_state(tag, "test-plan", "http://127.0.0.1:9"); let key = login_key(st.clone()).await; - let pad = "a".repeat(3 * 1024 * 1024); + let pad = "a".repeat(PROBE); let body = format!(r#"{{"model":"no-such-model","pad":"{pad}"}}"#); + drop(pad); let (status, bytes) = post_raw(st, uri, &body, Some(&key)).await; let msg = String::from_utf8_lossy(&bytes); assert_ne!( status, StatusCode::PAYLOAD_TOO_LARGE, - "{uri}: 3 MiB 的体被提取器的默认上限拒了(改前即此断言失败)" + "{uri}: {PROBE} 字节的体被上限拒了" ); // 体确实进了 handler:拿到的是网关自己的「无可用 key」而不是提取器的 413 assert!( @@ -1147,6 +1189,43 @@ mod tests { } } + /// 外层 `RequestBodyLimitLayer` 不得成为**比提取器更低**的那个钳制(2026-09-21)。 + /// + /// 为什么必须单独钉:两层是**两个数**。外层不写 `DefaultBodyLimitKind` 扩展,所以它 + /// 抬不动提取器;但它会在 `Content-Length > 自己的 limit` 时**不读体直接 413** + /// (`tower-http-0.5.2/src/limit/service.rs::call`)⇒ 只要外层更小,实际生效的就是它。 + /// 抬 `GATEWAY_BODY_LIMIT` 时外层正停在 `70 * 1024 * 1024`:**71 MiB 的体在旧外层上被 + /// 抢答 413**,而上限写着 277 MiB —— 「配了一个值 ≠ 生效了」的第二次现身。 + /// + /// 探针取 71 MiB(> 旧外层的 70 MB)。改前这条腿红在**外层**(413、纯文本 + /// `length limit exceeded`),改后走到 handler 的 503。 + #[tokio::test] + async fn the_outer_limit_does_not_clamp_below_the_gateway_limit() { + const PROBE: usize = 71 * 1024 * 1024; + const { + assert!( + PROBE > 70 * 1024 * 1024, + "探针必须大于旧外层的 70 MB,否则这条腿是空的" + ) + }; + let st = test_state("bl_outer", "test-plan", "http://127.0.0.1:9"); + let key = login_key(st.clone()).await; + let pad = "a".repeat(PROBE); + let body = format!(r#"{{"model":"no-such-model","pad":"{pad}"}}"#); + drop(pad); + let (status, bytes) = post_raw_len(st, "/v1/chat/completions", body, Some(&key)).await; + let msg = String::from_utf8_lossy(&bytes); + assert_ne!( + status, + StatusCode::PAYLOAD_TOO_LARGE, + "外层把 {PROBE} 字节的体抢答成 413 了(外层 limit 与 GATEWAY_BODY_LIMIT 不同源):{status} {msg}" + ); + assert!( + msg.contains("暂无可用 key"), + "期望走到 handler,实际 {status} {msg}" + ); + } + /// 负对照:同一次改动**不得**顺带放宽未认证端点。 #[tokio::test] async fn auth_endpoints_stay_at_the_default_limit() { @@ -1157,7 +1236,7 @@ mod tests { assert_eq!( status, StatusCode::PAYLOAD_TOO_LARGE, - "/api/auth/login 不该被放宽(未认证端点缓冲 8 MiB 是另一种风险)" + "/api/auth/login 不该被放宽(未认证端点缓冲大请求体是另一种风险)" ); } diff --git a/src/main.rs b/src/main.rs index 80d03bb..f93e153 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,10 @@ mod deploy_gate; // 加 #[cfg(test)] 后不会进入发布产物。 #[cfg(test)] mod state_gate; +// 请求体上限门禁(2026-09-21):同样是仅测试期编译 —— src/*.rs 在测试期读一遍, +// 加 #[cfg(test)] 后不会进入发布产物。 +#[cfg(test)] +mod body_limit_gate; use std::sync::Arc; @@ -234,11 +238,10 @@ async fn main() -> anyhow::Result<()> { let state = routes::AppState::new(conn, cfg, crypto); let app = routes::router() .with_state(state) - // 请求体上限:axum 默认 2MB,对超长 LLM 上下文/图片 base64 不够,放宽到 70MB - //(rant 2026-08-22T23:20:00;真实限制仍由上游模型 context 窗口裁决) - .layer(tower_http::limit::RequestBodyLimitLayer::new( - 70 * 1024 * 1024, - )) + // 请求体上限(连同外层粗闸)在 `routes::router()` 里与 `routes::GATEWAY_BODY_LIMIT` + // 绑在一起。这里**曾经**另写一层 `RequestBodyLimitLayer::new(70 * 1024 * 1024)`: + // 它是独立的一个数,抬上层就会被它悄悄钳住(rant 2026-09-18T09:14:18 的同族缺陷), + // 故移走 —— 上限只有一个数,见 `body_limit_gate.rs`。 .layer(tower_http::trace::TraceLayer::new_for_http()); let listener = tokio::net::TcpListener::bind(&addr).await?; diff --git a/src/routes/mod.rs b/src/routes/mod.rs index e3fc1a6..e219d29 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -37,18 +37,30 @@ use crate::router::RouterState; /// 上游请求的时限(连接与读取共用同一个数字,沿用 P0-B 写下的 120 s)。 pub(crate) const UPSTREAM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120); -/// 网关请求体的上限(rant 2026-09-18T09:14:18):三条网关路由各自挂一层 -/// `DefaultBodyLimit::max(8 MiB)`。 +/// 网关请求体的上限(rant 2026-09-18T09:14:18;2026-09-21 由 8 MiB 抬到 277 MiB)。 /// -/// 为什么 `main.rs` 里那层 `RequestBodyLimitLayer(70MB)` 不算数:它由 tower-http 提供, -/// 不写 axum 用来覆盖默认值的那条请求扩展(`DefaultBodyLimitKind`);而 `String` / `Json` -/// 等提取器的 2 MiB 上限来自 `axum-core` 的 `DEFAULT_LIMIT`,只认那条扩展。所以那层是 -/// **惰性的** —— 实测 2 MiB + 1 字节即 413,与它写的 70 MB 无关。抬上限只能用 axum -/// 自己的 `DefaultBodyLimit`。 +/// 这个数是**两层**的唯一来源,两层必须相等: /// -/// 为什么**不**全局挂:认证 / 注册 / 找回密码等未认证端点若也放宽到 8 MiB,等于给匿名 -/// 请求一个 8 MiB 的内存放大面;只有网关三条路由需要大请求体(1M token 上下文的对话)。 -pub(crate) const GATEWAY_BODY_LIMIT: usize = 8 * 1024 * 1024; +/// 1. 三条网关路由各自的 `DefaultBodyLimit::max(GATEWAY_BODY_LIMIT)` —— axum 提取器 +/// 真正读取的那一层。`String` / `Json` 等的 2 MiB 上限来自 `axum-core` 的 +/// `DEFAULT_LIMIT`,只认 `DefaultBodyLimitKind` 扩展,因此**只有 axum 自己的 +/// `DefaultBodyLimit` 抬得动它**(这就是原始缺陷:tower-http 那层不写该扩展)。 +/// 2. `router()` 末尾那层全局 `RequestBodyLimitLayer::new(GATEWAY_BODY_LIMIT)` —— +/// 外层粗闸,`Content-Length` 超限时**不读体直接 413**(body 是纯文本 +/// `length limit exceeded`,不带提取器那句前缀)。 +/// +/// ⚠️ **外层更小的话,实际生效的就是外层**:它抬不动提取器,但会**抢答**。2026-09-21 +/// 把本常量从 8 MiB 抬到 277 MiB 时,外层还停在 `70 * 1024 * 1024`,实测 71 MiB 的体 +/// 在外层就被 413 掉 ⇒ 抬了个寂寞。故外层也改成引用本常量,由 `body_limit_gate.rs` +/// 钉住「全树只有一个请求体上限的数」。 +/// +/// 为什么**不**全局放宽提取器:认证 / 注册 / 找回密码等未认证端点若也放宽,等于给匿名 +/// 请求一个同等大小的内存放大面;只有网关三条路由需要大请求体(长上下文 / 多模态)。 +/// +/// ⚠️ **内存代价**:提取器把请求体整个缓冲进内存(`String` / `Json`),转发前还会 +/// `body.clone()` 一次 ⇒ 单请求峰值约为本值的 2~3 倍。**改这个数必须连同宿主的可用内存 +/// 一起看**(prod 主机 1.8 GiB、无 swap、应用容器未设 `mem_limit`)。 +pub(crate) const GATEWAY_BODY_LIMIT: usize = 277 * 1024 * 1024; /// 非流式出站客户端:`timeout` 是**总**时限(建连到读完响应体),适合一次性响应。 pub(crate) fn upstream_client(timeout: std::time::Duration) -> reqwest::Client { @@ -693,6 +705,13 @@ pub fn router() -> Router { .route("/api/ops/users", get(ops::users)) // P2-A:静态托管(ui/ 目录;API 路由优先,未命中回退到文件服务) .fallback_service(tower_http::services::ServeDir::new("ui")) + // 请求体的**外层粗闸**(全路由):只对 `Content-Length` 做判断,超限则不读体 + // 直接 413。它**不是**上限的权威 —— 权威是三条网关路由上的 `DefaultBodyLimit` + // (`String` / `Json` 的 2 MiB 默认值只认那条扩展,这层抬不动它)。但若这里写 + // 得更小,就会抢先 413 ⇒ 两层共用 `GATEWAY_BODY_LIMIT` 这一个数。 + .layer(tower_http::limit::RequestBodyLimitLayer::new( + GATEWAY_BODY_LIMIT, + )) } #[cfg(test)]