Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrong error when body limit is hit while parsing a multipart upload #3216

Open
1 task done
jmg-duarte opened this issue Feb 10, 2025 · 1 comment
Open
1 task done

Comments

@jmg-duarte
Copy link

jmg-duarte commented Feb 10, 2025

  • 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

@EliasDerHai
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants