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 + '--'.