Upload a file via POST with Net::HTTP

To upload a file to a website I needed to supply the data with a content type of “multipart/form-data”. The Net:HTTP API does not offer any such functionality, it just accepts raw content data. So I needed to roll my own.

The “multipart/form-data” content type consists of a number of secions separated by --BOUNDARY\r\n and terminated by BOUNDARY--\r\n where BOUNDARY is a string that does not appear in the content of any of the data transmitted to the server.

Each section represents a form field and contains a number of headers, a \r\n, the content and finishes with a \r\n. Normal form fields look like

Content-Disposition: form-data; name="mykey"

mydata

while file fields must include a few more headers.

Content-Disposition: form-data; name="mykey"; filename="filename"
Content-Transfer-Encoding: binary
Content-Type: text/plain

DATADATADATADATADATADATADATA...

To construct the parameters in ruby I use the following code;

def text\_to\_multipart(key,value)
return “Content-Disposition: form-data; name=\\”\#{CGI::escape(key)}\\“\\r\\n” +
“\\r\\n” +
“\#{value}\\r\\n”
end

def file\_to\_multipart(key,filename,mime\_type,content)
return “Content-Disposition: form-data; name=\\”\#{CGI::escape(key)}\\“; filename=\\”\#{filename}\\“\\r\\n” +
“Content-Transfer-Encoding: binary\\r\\n” +
“Content-Type: \#{mime\_type}\\r\\n” +
“\\r\\n” +
“\#{content}\\r\\n”
end

To put it all together you need to join the parameters with boundary separators between each section. This can be done via

boundary = ‘349832898984244898448024464570528145’
query =
params.collect {|p| ‘—’ + boundary + “\\r\\n” + p}.join(’’) + “—” + boundary + “—\\r\\n”

The last thing that needs to be done is to make sure that you set the HTTP Header Content-type to multipart/form-data; boundary=BOUNDARY.

A complete example that I extracted from code that uploads a css file to the w3c validator service is as follows.

params = \[
file\_to\_multipart(‘file’,‘file.css’,‘text/css’,data),
text\_to\_multipart(‘warning’,‘1’),
text\_to\_multipart(‘profile’,‘css2’),
text\_to\_multipart(‘usermedium’,‘all’) \]

boundary = ‘349832898984244898448024464570528145’
query =
params.collect {|p| ‘—’ + boundary + “\\r\\n” + p}.join(’’) + “—” + boundary + “—\\r\\n”

response = http.start(‘jigsaw.w3.org’).
post2(“/css-validator/validator”,
query,
“Content-type” => “multipart/form-data; boundary=” + boundary)

It was a little bit painful to figure out “multipart/form-data” via Ethereal but relatively easy to implement. Hope this helps!

Update 3rd of October, 2006:

Slight correction supplied by Andrew Willis so that last boundary is ‘—’ + boundary + ‘—’ rather than just boundary + ‘—’.