benoitc/hackney

[Documentation] Hackney multipart/form-data. How to construct a POST request with multipart/form-data as Content-Type

Opened this issue · 10 comments

Hello,

I am trying to make a POST request with Content-Type: multipart/form-data but cant find any documentation about how to construct such a request body.

Postman:
image
image

Really appreciate your help!

You can post with a property list. Just pass th body {form, Props} where each Key, Values are binaries. That should be enough :)

Thanks @benoitc for the quick response,

But in my case, I have a file upload, which is in document_file. So I think I must use multipart/form-data?

Dear @benoitc,

Please correct me if I am wrong.

Since the request I am making has 2 fields, one of them is a file. So, as far as I know, I can use neither {form, Props} nor {file, File} ({file, File} is just for a single file without other fields (text fields)).

Postman body:
image

So currently I should have the following body for multipart

FName = hackney_bstr:to_binary(filename:basename(<<"ibrowse_tmp_file_157091908290916">>)),
MyName = <<"document_file">>,
Disposition = {<<"form-data">>,
	[{<<"name">>, <<"\"", MyName/binary, "\"">>},
	{<<"filename">>, <<"\"", FName/binary, "\"">>}]},
ExtraHeaders = [],
Body = {multipart, [
	{file, <<"ibrowse_tmp_file_157091908290916">>, Disposition, ExtraHeaders}, 	
	{<<"document_type">>, <<"ABC">>}
	]}
-----> Body is the body of the hackney:request()

But this seems not working!

Really appreciate your help!

I log the request on the server and only receive the file but the text fied (which is document_type). Still working on it

Dear @benoitc,

For text fields in multipart/form-data, in my opinion, it is not necessary to have explicit content-type (comparing to other data).

After spending time reading the source code of hackney, I found that if a part (in multipart) is pattern-matched into {Name, Bin} (which is {<<"document_type">>, <<"ABC">>} in my case), the Name is used to determine the content-type of this part using mimerl:filename(Name), which will result in application/octet-stream as default because <<"document_type">> has no extension. Thus makes the request wrong.

My workaround is that I temporarily remove the content-type header for mp_data_header
image Currently, it works as I expect.

Please tell me your thought.
Thanks in advance.

fwiw, I got file upload working with the following:

    FName = hackney_bstr:to_binary(filename:basename(Path)),
    Disposition = {<<"form-data">>,
        [{name, <<"field_name">>}, {filename, FName}]},
    ReqBody = {multipart, [
                           {file, hackney_bstr:to_binary(Path), Disposition, []}
                          ]},
    {ok, 200, _Hdrs, ResBody} = hackney:post(Url, [], ReqBody, [with_body]),

I can then parse it out using cowboy (1.0.4, because reasons):

read_form(Req, Acc) ->
    case cowboy_req:part(Req) of
        {ok, Headers, Req2} ->
            case cow_multipart:form_data(Headers) of
                {file, Field, _FName, _ContentType = <<"application/octet-stream">>, _TE} ->
                    {ok, Data, Req3} = cowboy_req:part_body(Req2),
                    read_form(Req3, Acc#{ Field => Data });
                {data, Field} ->
                    {ok, Data, Req3} = cowboy_req:part_body(Req2),
                    read_form(Req3, Acc#{ Field => Data })
            end;
        {done, Req2} ->
            {Acc, Req2}
    end.

@benoitc The problem now is that hackney lib doesn't seem to work w/making requests against express/multer middleware. This is costing me as I tried every format possible yet I'm actually using Httpoison in Elixirlang. I have no problem with the files but no form-data body params are being parsed. Looks like it may be a problem with "Content-Disposition" in the extra headers...

files =
dir
|> File.ls!()
|> Enum.map(fn filename ->
		{:file, @root_dir <> "#{id}/" <> filename, {"form-data", [name: "files[]", filename: filename]}, []}
end)

case HTTPoison.post(
		   apiv1_host <> storage_path,
		   {:multipart, files ++ [{"id", id, ["content-disposition": ~s(form-data; name="id")]}, {"title", opp_name, ["content-disposition": ~s(form-data; name="title")]}]}
		 ) do
{:ok, res} ->
		Logger.info("#{inspect(res.status_code)}")
		:ok

{:error, error} ->
		Logger.error("#{inspect(error)}")
		:error
end

Now to me.... that should work. Otherwise what's going on w/ExtraHeaders as a param here?

Looks like @npkhoa2197 solved the problem if what he says is true.... It's probably why busboy cannot parse requests from Hackney correctly. I guess nobody is really using Express but I have many APIs and don't feel like re-writing everything tonight...

Three cheers to @npkhoa2197 as he nailed it @benoitc. @edgurgel In the Httpoison lib there is a problem interfacing with multer using just {Name, Content} It results in multipart being stamped incorrectly. You MUST use {Name, Content, ExtraHeaders} in all cases to properly adhere to multipart/form-data API specification. See below.

{:multipart, files ++ [{"id", id, ["content-type": "text/plain", "content-disposition": ~s(form-data; name="id")]}, {"title", opp_name, ["content-type": "text/plain", "content-disposition": ~s(form-data; name="title")]}]}