Formatting Multipart Formdata in Erlang

January 10, 2010. Filed under erlang 20

I was having trouble finding examples of formatting a multipart formdata request in Erlang which allowed the sending of multiple post parameters along with one or more attached files in a single message. The http module solves a number of problems, but I couldn't figure out a way it solved this one.

Fortunately it was easy enough to find a reference implementation in Python and I somewhat crudely copied it over into Erlang. The end result is below.

%% @doc encode fields and file for HTTP post multipart/form-data.
%% @reference Inspired by <a href="">Python implementation</a>.
format_multipart_formdata(Boundary, Fields, Files) ->
    FieldParts = lists:map(fun({FieldName, FieldContent}) ->
                                   [lists:concat(["--", Boundary]),
                                    lists:concat(["Content-Disposition: form-data; name=\"",atom_to_list(FieldName),"\""]),
                           end, Fields),
    FieldParts2 = lists:append(FieldParts),
    FileParts = lists:map(fun({FieldName, FileName, FileContent}) ->
                                  [lists:concat(["--", Boundary]),
                                   lists:concat(["Content-Disposition: format-data; name=\"",atom_to_list(FieldName),"\"; filename=\"",FileName,"\""]),
                                   lists:concat(["Content-Type: ", "application/octet-stream"]),
                          end, Files),
    FileParts2 = lists:append(FileParts),
    EndingParts = [lists:concat(["--", Boundary, "--"]), ""],
    Parts = lists:append([FieldParts2, FileParts2, EndingParts]),
    string:join(Parts, "\r\n").

Usage looks like:

Data = binary_to_list(file:read_file("/path/to/a/file")),
URL = "",
Boundary = "------------a450glvjfEoqerAc1p431paQlfDac152cadADfd",
Body = format_multipart_formdata(Boundary, [{task_id, TaskId}, {position, Position}], [{file, "file", Data}]),
ContentType = lists:concat(["multipart/form-data; boundary=", Boundary]),
Headers = [{"Content-Length", integer_to_list(length(Body))}],
http:request(post, {URL, Headers, ContentType, Body}, [], []).

Is there a better way to do this somewhere in the http module? I'd be glad to know of it and to throw out this hacky and not-exactly-high-performance implementation.