HTTPoison POST multipart with more form than the file

I’m trying to build a function to send a file through a POST request in multipart format, using this as guide, but HTTPoison keeps giving me two errors no matter what changes I make to the form. They are all

HTTPoison.post("https://api.telegram.org/myCredentials", {:multipart, form}, headers)

and the versions of my form and the errors are the following (whether I use headers or not):

1st and 2nd Version (same error for both):

form = [{"photo", [{"name", "myphoto.jpg"}, {:file, "files/aphoto.jpg"}]}, {"chat_id", 237799110}]
-----
form = [photo: [{"name", "myphoto.jpg"}, {:file, "files/aphoto.jpg"}], chat_id: 237799110]

Which give me this error:

** (FunctionClauseError) no function clause matching in anonymous fn/2 in :hackney_multipart.len_mp_stream/2
      (hackney) c:/Users/venta/projects/elixir/wrapper/deps/hackney/src/hackney_multipart.erl:159: anonymous fn({"photo", [{"name", "myphoto.jpg"}, {:file, "files/aphoto.jpg"}]}, 0) in :hackney_multipart.len_mp_stream/2
       (stdlib) lists.erl:1263: :lists.foldl/3
      (hackney) c:/Users/venta/projects/elixir/wrapper/deps/hackney/src/hackney_multipart.erl:159: :hackney_multipart.len_mp_stream/2
      (hackney) c:/Users/venta/projects/elixir/wrapper/deps/hackney/src/hackney_request.erl:319: :hackney_request.handle_body/4
      (hackney) c:/Users/venta/projects/elixir/wrapper/deps/hackney/src/hackney_request.erl:81: :hackney_request.perform/2
      (hackney) c:/Users/venta/projects/elixir/wrapper/deps/hackney/src/hackney.erl:373: :hackney.send_request/2
    (httpoison) lib/httpoison/base.ex:432: HTTPoison.Base.request/9

I checked the code for Hackney and the line that is erroring out is one that is supposed to get the file and an accSize, but it gets passed the whole form, not the file, and Hackney can’t do anything with it. I’m wondering how to pass a form with a file inside, since the API I’m using requires that the file be inside a “photo” object as such: {“chat_id”: someid, “photo”: file_in_multipart/form-data, “otherkeys”: othervals}

2 Likes

I have also been battling with a multipart post recently and I have ended up with this as the solution which should hopefully help you out.

data = [
      {"to", to},
      {"subject", subject},
      {"text", render_template("#{template}.text", data)},
      {"html", render_template("#{template}.html", data)},
      {:file, "/some/file.jpg",
          {"form-data", [{"name", "inline"}, {"filename", "some-file-name.jpg"}]},
          [{"Content-Type", "image/jpg"}]
        }
    ]

This is what my data structure ends up looking like before being passed into HTTPoison like so

HTTPoison.post!("url", {:multipart, data})

I hope this helps in some form or another.

4 Likes

Sorry, I deleted half the code that was irrelevant and added a commetary at the end. How would you put that file inside a nested form?

Edit: and it does help. I can see a pattern. I’ll try some more with it.
Edit2: I’ll do some testing with nodejs where I have managed to send multipart files to the API, see the response it gives using netcat and see if I can reproduce that with elixir using some way around the nesting, which is what seems to be stopping HTTPoison from working.

3 Likes

So I tested, and tested, and didn’t manage to do what I set out to do. Here are my results:

With Nodejs, I get a successful post, as I said before. Here is what it looks like:

POST / HTTP/1.1
host: servIP:8080
content-type: multipart/form-data; boundary=--------------------------374763712004028780220119
content-length: 54609
Connection: close

----------------------------374763712004028780220119
Content-Disposition: form-data; name="chat_id"

237799109
----------------------------374763712004028780220119
Content-Disposition: form-data; name="photo"; filename="photo.jpg"
Content-Type: image/jpg

<photo binary>
----------------------------374763712004028780220119--

Here’s the JS code for the curious

As you can see, it is not a photo object containing a photo, but an item between boundaries named after the object that it’s supposed to be, so photo: binary becomes name=photo <content-type> <binary> and other elements of the form are just presented as different items with a boundary in-between. The boundary is pre-defined at the beginning and two hyphens are added at the start of each boundary and at the end of the multipart post.

Here is a list of all the results (I will summarize them later, so you don’t have to read the contents of the link):

The successful posts (successfully uploaded a file to the server) through HTTPoison never reach the same result. The two closest ones are:

Result A:

POST / HTTP/1.1
Host: servIP:8080
User-Agent: hackney/1.7.1
Content-Type: multipart/form-data; boundary=---------------------------kyorwiszyqolhtih
Content-Length: 883

-----------------------------kyorwiszyqolhtih
content-length: 631
content-type: image/jpeg
content-disposition: form-data; name="file"; filename="pixel.jpg"
name: "photo"
filename: "pixel.jpg"

<photo binary>
-----------------------------kyorwiszyqolhtih--

and

Result B:

POST / HTTP/1.1
Host: servIP:8080
User-Agent: hackney/1.7.1
Content-Type: multipart/form-data; boundary=---------------------------mgvbohsjfwhmfxty
Content-Length: 221

-----------------------------mgvbohsjfwhmfxty
content-length: 15
content-type: application/octet-stream
content-disposition: form-data; name="photo"

files/pixel.jpg
-----------------------------mgvbohsjfwhmfxty--

As you can see on Result A, the metadata that hackney is supposed to add is added not as proper multi-form content-disposition entries but as strange entries with colon notation as name: "photo" instead of name="photo", added after content-disposition. The second one, I didn’t ever manage to upload the file because I could only say {"key", "value"} because {"key": {:file, data}} was rejected by Hackney.

I have no idea how to solve this. Is HTTPoison unable to do this or is it just me being a hopeless noob?

1 Like

Sorry to bump an old thread, I’m sure the original poster figured it out, gave up, or moved onto something else, but in case anyone is interested/finds it useful, I believe this is the code he needs:

form = [
  {
    :file,
    "files/aphoto.jpg",
    {"form-data", [{"name", "photo"}, {"filename", "myphoto.jpg"}]},
    [] # pass along empty ExtraHeaders
  },
  {"chat_id", "237799110"}
]

HTTPoison.post("https://api.telegram.org/myCredentials", {:multipart, form}, headers)

The important part for providing a custom name to your file parameter within the form is the “name” param of “form-data”, as well as passing the empty ExtraHeaders param to ensure when creating the header for the file we go straight here:

https://github.com/benoitc/hackney/blob/e6d84f40a4d2d650659ac54dba99c0bcbbb86af4/src/hackney_multipart.erl#L232

Otherwise it will create default Content-Disposition form-data attributes:

3 Likes