Rust in Everything
基于 dioxus 0.7.10

请求体超过 2 MiB 返回 500

主线位置:调用主线的服务端解码一步。参数走 JSON body 的 server function,在 extract_axum 里先把整个 body 读成 Bytes,再 serde_json::from_slice。读 body 用的是 axum 的提取器,上限也是 axum 的;区别在于 dioxus 拿到 rejection 之后没有把它变成响应,而是 unwrap。

flowchart TB
  req["POST 3 MB JSON"] --> mw{"外层中间件"}
  mw -->|"超限 回 413"| r413["413"]
  mw -->|放行| ext["Bytes::from_request"]
  ext --> lim{"超过 DefaultBodyLimit"}
  lim -->|没超| fn1["解码 跑函数体"]
  lim -->|超了| pan["unwrap panic"]
  pan --> r500["JoinError 变 500"]

实验应用 pit2-fs(dioxus = "=0.7.10",dx 0.7.9,PORT=18820),M2 Max、macOS 27、rustc 1.99.0。请求体是 {"name":"a.png","data_base64":"AAAA..."},3 MB 和 9 MB 两份,curl --data-binary @big3.json。

现象

#[post("/api/upload")]
pub async fn upload(name: String, data_base64: String) -> Result<String, ServerFnError> {
  Ok(format!("{name}: {} bytes of base64", data_base64.len()))
}

debug 构建:

### 3 MB JSON to /api/upload
HTTP/1.1 100 Continue

HTTP/1.1 500 Internal Server Error
content-length: 192
connection: close

Server function panicked: task 98 panicked with message "called `Result::unwrap()` on an `Err` value: FailedToBufferBody(LengthLimitError(LengthLimitError(Error { inner: LengthLimitError })))"

服务端 stderr:

thread '<unnamed>' (2081594) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/dioxus-fullstack-0.7.10/src/magic.rs:542:69:

release 构建,同一个请求:

### 3 MB JSON to /api/upload
HTTP/1.1 100 Continue

HTTP/1.1 500 Internal Server Error
content-length: 21
connection: close

Internal Server Error

任何参数走 body 的 server function 都一样,不只是上传。客户端拿到的是 500,看不出是「太大了」,函数体一行都没跑,函数里自己写的大小检查(「文件过大」)永远轮不到。

出处

extract_axum 里取 body 的一行:

                let bytes = Bytes::from_request(request, &()).await.unwrap();

axum-core 的 Bytes::from_request 读 body 时套一个 Limited,请求的 extensions 里没有 DefaultBodyLimit 时上限是 2 MiB(2 097 152 字节),超了返回 FailedToBufferBody::LengthLimitError。普通 axum handler 会把这个 rejection 渲染成 413(axum 卷的 Failed to buffer the request body: length limit exceeded);这里是 unwrap,任务 panic。make_handler 用 spawn_pinned 跑任务,panic 变成 JoinError,match result 回 500,debug 带 panic 消息,release 只有 Internal Server Error。读 body 的字节怎么流动、Limited 怎么数,bytes 卷的 server 函数的请求体 拆过。

dioxus-fullstack 0.7.10 和 dioxus-server 0.7.10 都没有设 DefaultBodyLimit,所以默认就是 axum 的 2 MiB。

修法

三步,按需要选。外层都是 dioxus::serve 返回的 Router:

const UPLOAD_LIMIT: usize = 8 * 1024 * 1024;
const DEFAULT_LIMIT: usize = 2 * 1024 * 1024;

dioxus::serve(|| async move {
  let router = axum::Router::new()
    .serve_dioxus_application(ServeConfig::new(), app)
    // ... one of the layers below
    ;
  Ok(router)
})

(axum 是 dioxus::server::axum,serve_dioxus_application 来自 prelude 里的 DioxusRouterExt。)

一:调高上限

router.layer(DefaultBodyLimit::max(UPLOAD_LIMIT))

DefaultBodyLimit 只往请求的 extensions 里写一个值,dioxus 里那句 Bytes::from_request 读的就是它,所以套在外面也有效:

### 3 MB JSON to /api/upload
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
content-type: application/json
content-length: 32

"a.png: 3000000 bytes of base64"

### 9 MB JSON to /api/upload
HTTP/1.1 100 Continue

HTTP/1.1 500 Internal Server Error
content-length: 192
connection: close

Server function panicked: task 20 panicked with message "called `Result::unwrap()` on an `Err` value: FailedToBufferBody(LengthLimitError(LengthLimitError(Error { inner: LengthLimitError })))"

### 3 MB JSON to /api/admin/delete
HTTP/1.1 100 Continue

HTTP/1.1 500 Internal Server Error
content-type: application/json
content-length: 100

{"error":"error deserializing server function results: missing field `id` at line 1 column 3000036"}

3 MB 的上传过了;9 MB 又是 panic。门槛挪了,unwrap 还在。整个路由一起放宽,别的函数也收 8 MiB 的 body 了(最后一条读完 3 MB 才报参数错)。

二:按 Content-Length 先拒绝

// Reject by Content-Length before dioxus reads the body.
async fn reject_large(req: Request, next: Next) -> Response {
  let limit = if req.uri().path() == "/api/upload" { UPLOAD_LIMIT } else { DEFAULT_LIMIT };
  let declared = req
    .headers()
    .get(CONTENT_LENGTH)
    .and_then(|v| v.to_str().ok())
    .and_then(|v| v.parse::<usize>().ok());
  if declared.is_some_and(|n| n > limit) {
    return (StatusCode::PAYLOAD_TOO_LARGE, "payload too large").into_response();
  }
  next.run(req).await
}

router
  .layer(DefaultBodyLimit::max(UPLOAD_LIMIT))
  .layer(middleware::from_fn(reject_large))
### 9 MB JSON to /api/upload
HTTP/1.1 413 Payload Too Large
content-type: text/plain; charset=utf-8
content-length: 17

payload too large

### 3 MB JSON to /api/admin/delete
HTTP/1.1 413 Payload Too Large
content-type: text/plain; charset=utf-8
content-length: 17

payload too large

### 9 MB JSON to /api/upload, chunked (no Content-Length)
HTTP/1.1 100 Continue

HTTP/1.1 500 Internal Server Error
content-length: 192
connection: close

Server function panicked: task 22 panicked with message "called `Result::unwrap()` on an `Err` value: FailedToBufferBody(LengthLimitError(LengthLimitError(Error { inner: LengthLimitError })))"

带 Content-Length 的超限请求在头部就被拒绝(没有 100 Continue,客户端不用把 9 MB 传完),每条路径用自己的上限。分块传输的请求没有 Content-Length,照样走到 unwrap。tower-http 的 RequestBodyLimitLayer 在有 Content-Length 时也是这样先拒绝(axum 卷「修法」一节),本页没有测它对分块请求的表现。

三:自己带上限把 body 读完

// Read the body here, with the limit, so dioxus never sees an oversized one (works without Content-Length too).
async fn buffer_with_limit(req: Request, next: Next) -> Response {
  if !req.uri().path().starts_with("/api/") {
    return next.run(req).await;
  }
  let limit = if req.uri().path() == "/api/upload" { UPLOAD_LIMIT } else { DEFAULT_LIMIT };
  let (parts, body) = req.into_parts();
  match axum::body::to_bytes(body, limit).await {
    Ok(bytes) => next.run(Request::from_parts(parts, axum::body::Body::from(bytes))).await,
    Err(_) => (StatusCode::PAYLOAD_TOO_LARGE, "payload too large").into_response(),
  }
}

router
  .layer(DefaultBodyLimit::max(UPLOAD_LIMIT))
  .layer(middleware::from_fn(buffer_with_limit))
### 3 MB JSON to /api/upload
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
content-type: application/json
content-length: 32

"a.png: 3000000 bytes of base64"

### 9 MB JSON to /api/upload
HTTP/1.1 100 Continue

HTTP/1.1 413 Payload Too Large
content-type: text/plain; charset=utf-8
content-length: 17

payload too large

### 3 MB JSON to /api/admin/delete
HTTP/1.1 100 Continue

HTTP/1.1 413 Payload Too Large
content-type: text/plain; charset=utf-8
content-length: 17

payload too large

### 9 MB JSON to /api/upload, chunked (no Content-Length)
HTTP/1.1 100 Continue

HTTP/1.1 413 Payload Too Large
content-type: text/plain; charset=utf-8
content-length: 17

payload too large

四种都对了,分块请求也是 413。代价是 body 在中间件里先整个读进内存,再交给 dioxus 读第二遍,内存上限就是你给的 limit。中间件里的上限要和 DefaultBodyLimit 配好:中间件放行的最大值不能超过 DefaultBodyLimit,否则又回到 unwrap。

只有确实需要大 body 的路径放宽;更大的文件不要整个进 JSON,用 payloads 里的流或 multipart 类型(server function 列了它们)。本站在 dioxus 之前回 413 的做法见本站的页面。

怎么确认没中招

  • 对每条参数走 body 的 server function 发一个 3 MB 的请求、再发一个分块的(-H 'Transfer-Encoding: chunked'),断言 413 而不是 500。
  • 服务端日志里搜 magic.rs:542:出现一次,就是有一个超限请求走到了 unwrap。
  • 上传接口另测「刚好在上限内」的大小:base64 让体积变成 4/3,再加 JSON 外壳,5 MB 的文件是 6.7 MB 左右的请求体。

小结

  • 参数走 JSON body 的 server function 用 Bytes::from_request(..).await.unwrap() 读 body,超过 axum 默认的 2 MiB 就 panic,回 500(debug 带 panic 消息)。
  • DefaultBodyLimit::max 套在外层 Router 上有效,但只是把 panic 的门槛挪高。
  • 按 Content-Length 先回 413 挡不住分块请求;在中间件里用 to_bytes(body, limit) 自己读完才全挡住,中间件的上限不能超过 DefaultBodyLimit。

💬 关联讨论

发起讨论
加载中...