Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to post an image using multipart/form-data with hyper?

Tags:

post

rust

hyper

I'm trying to post an image file using hyper like cURL does:

curl -F [email protected] https://httpbin.org/post --trace-ascii -

The result is:

{
  "args": {},
  "data": "",
  "files": {
    "smfile": "data:image/jpeg;base64,..."
  },
  "form": {},
  "headers": {
    "Accept": "/",
    "Connection": "close",
    "Content-Length": "1709",
    "Content-Type": "multipart/form-data; boundary=------------------------58370e136081470e",
    "Expect": "100-continue",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.59.0"
  },
  "json": null,
  "origin": "myip",
  "url": "https://httpbin.org/post"
}

I learned that Content-Type should be set to multipart/form-data with a boundary mark. Here's my code:

extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate tokio;

use futures::{future, Future};
use hyper::header::CONTENT_TYPE;
use hyper::rt::Stream;
use hyper::{Body, Client, Method, Request};
use hyper_tls::HttpsConnector;
use std::fs::File;
use std::io::prelude::*;
use std::io::{self, Write};

const BOUNDARY: &'static str = "------------------------ea3bbcf87c101592";

fn main() {
    tokio::run(future::lazy(|| {
        let https = HttpsConnector::new(4).unwrap();
        let client = Client::builder().build::<_, hyper::Body>(https);

        let mut req = Request::new(Body::from(image_data()));

        req.headers_mut().insert(
            CONTENT_TYPE,
            format!("multipart/form-data; boundary={}", BOUNDARY)
                .parse()
                .unwrap(),
        );
        *req.method_mut() = Method::POST;
        *req.uri_mut() = "https://httpbin.org/post".parse().unwrap();

        client
            .request(req)
            .and_then(|res| {
                println!("status: {}", res.status());

                res.into_body().for_each(|chunk| {
                    io::stdout()
                        .write_all(&chunk)
                        .map_err(|e| panic!("stdout error: {}", e))
                })
            })
            .map_err(|e| println!("request error: {}", e))
    }));
}

fn image_data() -> Vec<u8> {
    let mut result: Vec<u8> = Vec::new();
    result.extend_from_slice(format!("--{}\r\n", BOUNDARY).as_bytes());
    result
        .extend_from_slice(format!("Content-Disposition: form-data; name=\"text\"\r\n").as_bytes());
    result.extend_from_slice("title\r\n".as_bytes());
    result.extend_from_slice(format!("--{}\r\n", BOUNDARY).as_bytes());
    result.extend_from_slice(
        format!("Content-Disposition: form-data; name=\"smfile\"; filename=\"11.jpg\"\r\n")
            .as_bytes(),
    );
    result.extend_from_slice("Content-Type: image/jpeg\r\n\r\n".as_bytes());

    let mut f = File::open("11.jpg").unwrap();
    let mut file_data = Vec::new();
    f.read_to_end(&mut file_data).unwrap();

    result.append(&mut file_data);

    result.extend_from_slice(format!("--{}--\r\n", BOUNDARY).as_bytes());
    result
}

(complete code)

Note that a JPEG file named 11.jpg is needed to run this code. This can be any JPEG file.

httpbin shows that I posted nothing:

{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Connection": "close",
    "Content-Length": "1803",
    "Content-Type": "multipart/form-data; boundary=------------------------ea3bbcf87c101592",
    "Host": "httpbin.org"
  },
  "json": null,
  "origin": "myip",
  "url": "https://httpbin.org/post"
}

I have no idea how to fix this.

like image 354
user2986683 Avatar asked Jul 18 '18 09:07

user2986683


People also ask

How do I add an image to a multipart?

To do this, post the photo to the Memories resource with the Content-Type set to multipart/form-data . Provide the description of each resource in the first part. Include the title and the qualifiers and any other metadata. Provide the images in each subsequent part with the Content-Type set to image/jpeg .

How do I upload a file with multipart form data?

Multipart form data: The ENCTYPE attribute of <form> tag specifies the method of encoding for the form data. It is one of the two ways of encoding the HTML form. It is specifically used when file uploading is required in HTML form. It sends the form data to server in multiple parts because of large size of file.


1 Answers

You aren't correctly placing a newline/carriage return pair before your final boundary.

Here's how I'd write your body generation code, requiring less allocation:

fn image_data() -> io::Result<Vec<u8>> {
    let mut data = Vec::new();
    write!(data, "--{}\r\n", BOUNDARY)?;
    write!(data, "Content-Disposition: form-data; name=\"smfile\"; filename=\"11.jpg\"\r\n")?;
    write!(data, "Content-Type: image/jpeg\r\n")?;
    write!(data, "\r\n")?;

    let mut f = File::open("11.jpg")?;
    f.read_to_end(&mut data)?;

    write!(data, "\r\n")?; // The key thing you are missing
    write!(data, "--{}--\r\n", BOUNDARY)?;

    Ok(data)
}

Calling this code can also be simplified:

fn main() {
    let https = HttpsConnector::new(4).unwrap();
    let client = Client::builder().build::<_, hyper::Body>(https);

    let data = image_data().unwrap();
    let req = Request::post("https://httpbin.org/post")
        .header(CONTENT_TYPE, &*format!("multipart/form-data; boundary={}", BOUNDARY))
        .body(data.into())
        .unwrap();

    tokio::run(future::lazy(move || {
        client
            .request(req)
            .and_then(|res| {
                println!("status: {}", res.status());

                res.into_body().for_each(|chunk| {
                    io::stdout()
                        .write_all(&chunk)
                        .map_err(|e| panic!("stdout error: {}", e))
                })
            })
            .map_err(|e| println!("request error: {}", e))
    }));
}
like image 166
Shepmaster Avatar answered Nov 15 '22 10:11

Shepmaster