How to POST a file using reqwest?
Asked Answered
S

3

13

The documentation for reqwest v0.9.18 shows the following example of posting a file:

let file = fs::File::open("from_a_file.txt")?;
let client = reqwest::Client::new();
let res = client.post("http://httpbin.org/post")
    .body(file)
    .send()?;

The latest documentation for reqwest v0.11 no longer includes this example, and trying to build it fails with the following error when calling body():

the trait `From<std::fs::File>` is not implemented for `Body`

What is the updated method for sending a file?

Syne answered 20/1, 2021 at 17:21 Comment(2)
body requires bytes(?) so just dump the file to bytes?Lohse
I didn't want to de-focus the question, but if there's another library that makes this easy instead of reqwest I would be happy to switch. Ideally the file would be streamed instead of all being read into memory ahead of time.Syne
P
22

The specific example you're linking to, was prior to the reqwest crate using async. If you want to use that exact example, then instead of reqwest::Client, you need to use reqwest::blocking::Client. This also requires enabling the blocking feature.

To be clear, you can actually still find that example, it's just located in the docs for reqwest::blocking::RequestBuilder's body() method instead.

// reqwest = { version = "0.11", features = ["blocking"] }
use reqwest::blocking::Client;
use std::fs::File;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = File::open("from_a_file.txt")?;

    let client = Client::new();
    let res = client.post("http://httpbin.org/post")
        .body(file)
        .send()?;

    Ok(())
}

Also check out reqwest's Form and RequestBuilder's multipart() method, as there for instance is a file() method.


If you do want to use async, then you can use FramedRead from the tokio-util crate. Along with the TryStreamExt trait, from the futures crate.

Just make sure to enable the stream feature for reqwest, and the codec feature for tokio-util.

// futures = "0.3"
use futures::stream::TryStreamExt;

// reqwest = { version = "0.11", features = ["stream"] }
use reqwest::{Body, Client};

// tokio = { version = "1.0", features = ["full"] }
use tokio::fs::File;

// tokio-util = { version = "0.6", features = ["codec"] }
use tokio_util::codec::{BytesCodec, FramedRead};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = File::open("from_a_file.txt").await?;

    let client = reqwest::Client::new();
    let res = client
        .post("http://httpbin.org/post")
        .body(file_to_body(file))
        .send()
        .await?;

    Ok(())
}

fn file_to_body(file: File) -> Body {
    let stream = FramedRead::new(file, BytesCodec::new());
    let body = Body::wrap_stream(stream);
    body
}
Parthenia answered 20/1, 2021 at 18:35 Comment(2)
is there a way to connect some monitor on wrap_stream ? I am trying to make a progress bar for POST call like at bashupload.com/how_to_upload_progress_curlFigone
Is there a way to do this with Form in async, with multipart?Subgroup
D
5

If you want to use multipart/form-data and you are using Tokio already, this approach could help you.

1. Setup Dependencies

# Cargo.toml

[dependencies]
tokio = { version = "1.19", features = ["macros", "rt-multi-thread"] }
reqwest = { version = "0.11.11", features = ["stream","multipart","json"] }
tokio-util = { version = "0.7.3", features = ["codec"] }

2. Upload file using multipart/form-data

use reqwest::{multipart, Body, Client};
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};

async fn reqwest_multipart_form(url: &str) -> anyhow::Result<String> {
    let client = Client::new();
    let file = File::open(".gitignore").await?;

    // read file body stream
    let stream = FramedRead::new(file, BytesCodec::new());
    let file_body = Body::wrap_stream(stream);

    //make form part of file
    let some_file = multipart::Part::stream(file_body)
        .file_name("gitignore.txt")
        .mime_str("text/plain")?;

    //create the multipart form
    let form = multipart::Form::new()
        .text("username", "seanmonstar")
        .text("password", "secret")
        .part("file", some_file);

    //send request
    let response = client.post(url).multipart(form).send().await?;
    let result = response.text().await?;

    Ok(result)
}

3. Unit Testing

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_post_form_file() {
        let url = "http://httpbin.org/post?a=1&b=true";
        let get_json = reqwest_multipart_form(url).await.unwrap();

        println!("users: {:#?}", get_json);
    }
}
Disallow answered 2/7, 2022 at 11:47 Comment(4)
This is clearly worse than the accepted answer, have much noise and not required parts such as forms, and have no explanation.Cataclysmic
I find it nice, but I would include other use cases for upload if possible. Insightful approach IMHO.Talkathon
This is the only answer that actually worked for me; I needed multipart forms. Thank youDemitria
Same here. I needed multipart form. I understand that maybe it wasn't exactly the question but if you landed here because of googling this, it's a great answer.Paulettapaulette
A
0

the crate streamer can do that for you with feature hyper enabled:

use hyper::{Body, Request}:
let file = File::open("from_a_file.txt").unwrap();
let mut streaming = Streamer::new(file)
// optional, set the field name
// streaming.meta.set_name("txt"); 
// optional, set the file name
streaming.meta.set_filename("from_a_file.txt");
// length sent as a chunk, the default is 64kB if not set
streaming.meta.set_buf_len(1024 * 1024); 

let body: Body = streaming.streaming();
// build a request 
let request: Request<Body> = Request::post("<uri-here>").body(body).expect("failed to build a request");

streamer will stream your file in 1 Mega-bytes chunks

Antenatal answered 27/6, 2022 at 12:58 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.