请求体超过 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。