axum
axum copied to clipboard
Wrong error when body limit is hit while parsing a multipart upload
- [x] I have looked for existing issues (including closed) about this
Bug Report
multipart/form-data parsing failure on limited instead of a "limited" error
Version
jose.duarte@Mac t % cargo tree | grep axum
├── axum v0.8.1
│ ├── axum-core v0.5.0
Platform
Darwin Mac.lan 24.2.0 Darwin Kernel Version 24.2.0: Fri Dec 6 18:51:28 PST 2024; root:xnu-11215.61.5~2/RELEASE_ARM64_T8112 arm64
and
Linux parthenon 6.8.0-52-generic #53-Ubuntu SMP PREEMPT_DYNAMIC Sat Jan 11 00:06:25 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Crates
Description
Reproducer:
Code
use axum::{
extract::{DefaultBodyLimit, FromRequest, MatchedPath, Multipart, Path, Request},
http::StatusCode,
routing::put,
Router,
};
use futures_util::TryStreamExt;
use tokio_util::io::StreamReader;
use tower_http::trace::TraceLayer;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use uuid::Uuid;
#[tracing::instrument]
async fn upload(request: Request) -> Result<(), (StatusCode, String)> {
tracing::debug!("creating multipart");
let mut multipart = Multipart::from_request(request, &())
.await
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;
tracing::debug!("creating field");
let Some(reader) = multipart
.next_field()
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?
else {
return Err((StatusCode::BAD_REQUEST, "no field available".to_string()));
};
tracing::debug!("creating stream reader");
let mut reader = StreamReader::new(reader.map_err(std::io::Error::other));
tracing::debug!("creating/truncating file");
let mut file = tokio::fs::File::create("/tmp/t/test_file")
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
tracing::debug!("reading in stream");
tokio::io::copy(&mut reader, &mut file)
.await
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
Ok(())
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(fmt::layer().with_file(true).with_line_number(true))
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env()
.unwrap(),
)
.init();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
let router = Router::new()
.route(
"/upload",
put(upload), // .layer(DefaultBodyLimit::disable())
)
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
tracing::info_span!(
"request",
method = ?request.method(),
matched_path,
request_id = %Uuid::new_v4()
)
}),
);
axum::serve(listener, router).await.unwrap()
}
Cargo.toml
[package]
name = "t"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.8.1", features = ["multipart"] }
futures-util = "0.3.31"
tokio = { version = "1.43.0", features = ["full"] }
tokio-util = { version = "0.7.13", features = ["io"] }
tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["trace"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
uuid = { version = "1.13.1", features = ["v4"] }
I expected to see this happen: when uploading a large file using cURL, I was expecting the "limited" error.
Instead, this happened:
jose.duarte@Mac t % dd if=/dev/urandom of=1GB bs=1G count=1
jose.duarte@Mac t % curl -v -X PUT -F "upload=@1GB" 127.0.0.1:3000/upload
* Trying 127.0.0.1:3000...
* Connected to 127.0.0.1 (127.0.0.1) port 3000
> PUT /upload HTTP/1.1
> Host: 127.0.0.1:3000
> User-Agent: curl/8.7.1
> Accept: */*
> Content-Length: 1073742033
> Content-Type: multipart/form-data; boundary=------------------------VlVd06f5jOjyD4I9oh8pVm
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
<
< HTTP/1.1 500 Internal Server Error
< content-type: text/plain; charset=utf-8
< content-length: 43
< date: Fri, 07 Feb 2025 12:17:19 GMT
<
* HTTP error before end of send, stop sending
* abort upload after having sent 3914268 bytes
* Closing connection
Error parsing `multipart/form-data` request%
Additional notes
I'm available to help fix, I suspect that this error: https://github.com/tokio-rs/axum/blob/main/axum/src/extract/multipart.rs#L249
Is either not being hit or propagated upwards
Just learning async rust and already struggling with pin unpin etc. This issue really drove me into madness as the error is very far from being descriptive.
For anyone also stumbling upon this axum has an nice minimal example though https://github.com/tokio-rs/axum/blob/main/examples/multipart-form/src/main.rs
maybe: #2850
@jmg-duarte if you're motivated to fix this issue, you're welcome trying. Even a PR with a test reproducing the issue can help!