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 + ‘—’.