Unclonable variable being moved

async fn upload(mut multipart: Multipart) -> impl IntoResponse {
    while let Some(field) = multipart.next_field().await.unwrap() {
        let name = field.name().unwrap().to_string();
        let text = field.text().await;

        if text.is_err() {
            let filename = field.file_name().unwrap().to_string();
            let data = field.bytes().await.unwrap();
            services::file_service::save_upload(&filename, &data);
        } else {
            println!("{}: {}", name, text.unwrap());
        }
    }

    StatusCode::NO_CONTENT
}

Hi guys, I am new to rust world and love it so much.

Here is the code working on axum file upload.
What I am trying to do is getting the upladed files and parameters. I am checking on field.text() to see if it is ok or error then I can determine this field is text or file.

But this code does not compile due to moving of field variable. field.text().await moves field then field.file_name() complains.
Also field cannot be cloned.

What I can do with this scenario? Any help would be appreciated.

Which is, of course, wrong and it's very easy to see why right in your code. text is async function which means it does “something behind your back”. Namely: it actually receives data entered into your text field.

And the same happens with bytes. Of course you may only receive something once… and that's exactly what happens.

Worse: the whole logic here is flaved: why do you think that if someone uploads file there would be no text? Because file is binary and couldn't be interpereted as text? I can upload text file, no problem.

Usually you just use different handlers with text files and file uploads but if you really need to distinguish these then it's better to start from file_name (which doesn't consume the field) and then decide whether you want to get text or binary data. Where text, of course, is produced from said binary data if it can be represented as utf-8.

P.S. And thanks for describing what, as you think, happens in that code. As you see the whole premise on which said code is based was wrong (no, text is not “something that only text fields have”, that's just bytes represented as “utf-8”, if possible), but if you, like many others, would have just presented that code and asked “how make it compile” then it would have lead to a long and pointless discussion about why do you want that pointless code to compile.

Thanks for your classification. Now I just updated my code to get extra param from query string during file upload.

#[derive(Deserialize)]
struct UploadParam {
    id: String,
}

/// If you encounter error: `Result::unwrap()` on an `Err` value: MultipartError { source: failed to read stream }
/// Just check DefaultBodyLimit value setting in lib.rs.
async fn upload(Query(params): Query<UploadParam>, mut multipart: Multipart) -> impl IntoResponse {
    while let Some(field) = multipart.next_field().await.unwrap() {
        let filename = field.file_name().unwrap().to_string();
        let data = field.bytes().await.unwrap();
        services::file_service::save_upload(&filename, &data);
    }

    println!("id value: {}", params.id);

    StatusCode::NO_CONTENT
}

But I still like to know if unclonable variable needs to be passed to multiple external functions which all take ownership. Then what solution would we have?

This means that either you've got some kind of logic error, or the code you're calling into is overly restrictive.

In most cases, when function (or method) takes ownership of something, this means that it needs that ownership (for example, to move the value out of the struct or to ensure that some external process isn't called twice). If your logic requires to move the ownership twice - it usually means you have some kind of contradictive requirements; in this case it was "receive the request body from the socket when it's not there already". If that's not the case, the correct way to go would be to file an issue to the crate in question, asking whether some of the requirements can be relaxed.

2 Likes

Thank you so much for your explanation.

Note how that restriction that you can not call something on the moved out variable is actually an advantage.

Note how most languages couldn't prevent you from writing code like in first message yet said code would still be incorrect.

If your bytes or text message receives data from the socket then calling text then bytes (or bytes then text) doesn't make any sense no matter what language you use.

Only in most languages that would be some kind of runtime error (call to bytes would throw an exception, or, worse, would return an empty array) while in Rust that's compile-time error.

That's both blessing and a curse. It's absolute blessing if your goal is correct, working, reliable program. It's a curse when you want to experiment but compiler says “no, no, no… stop: this function would never work… make it correct even if you don't plan to actually use it just yet”.

The other thing that is being somewhat obscured by this struct, is that text is simply a (attempted) decoding of bytes. So if you had a real need to obtain both, what you would need to do is to call bytes to get a Bytes struct (which does implement Clone), and then attempt to decode it to text yourself.

Thanks for all of your replies.

In the first place I was not trying to get text and file in one iteration.

The scenario is there are multiple form data items in one post request. Let's say two files and one text item.

What I was trying to do is check on each item to see it is file or text value. Not getting file and text from same item.

Anyway I am using form data item to upload file and get text value from query string instead.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.