Upload file using Net::HTTP in Ruby

First, you'll need to know how the browser work on uploading files or how the HTTP request looks like when sending as upload files request.

To upload files in the browser, we use a form like this:

<form enctype="multipart/form-data" action="http://localhost:3000/" method="POST">
  <input type="hidden" name="MAX_FILE_SIZE" value="100000" />
  Choose a file to upload: <input name="uploadedfile" type="file" /><br />
  <input type="submit" value="Upload File" />

The browser will construct a multipart HTTP request message and send it to the webserver:

Host: localhost:3000
Content-Length: 1325
Origin: http://localhost:3000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L
<other headers>

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

Content-Disposition: form-data; name="uploadedfile"; filename="image.png"
Content-Type: image/png

<file data (image.png binary)>

Instead of URL encoding the form parameters, the form parameters (including the file data) are sent as sections in a multipart document in the body of the request.

As you can see in the above example, the multipart HTTP request message consists of a number of sections separated by a boundary=


Each section represent a set value in the form (form field) and it contains a number of headers, a \r\n, content (form field values) and finishes with a \r\n. So, the general normal field will look like this:

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


And for the file filed, it will look like this:

Content-Disposition: form-data; name="fieldname"; filename="filename"
Content-Type: file-mime-type


The request body will be terminated by boundary + "--".

The Net::HTTP ruby library only accepts raw content, which mean it will only accepts a string HTTP request. So, in order to trigger a post upload file request, we must build the request string message manually.

Let's build the multipart sections first. There are two type of sections: one is normal and the other is file part. We will need two method for each section:

Normal section

def multipart_text key, value
  content = "Content-Disposition: form-data; name=\"#{key}\"" <<
    "\r\n" << "\r\n" << "#{value}" << "\r\n"

File section

def multipart_file key, filename, mime_type, content
  content = "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"#{"\r\n"}" <<
    "Content-Type: #{mime_type}\r\n" << "\r\n" << "#{content}" << "\r\n"

These two method is encapsulate in a class called MultipartPost, which I will used to make the multipart request body and issuse a request to a web server. This class will have an array attribute to hold all the sections of the request and an attribute to hold the request url.

class MultipartPost
  EOL = "\r\n"

  def initialize uri, &block
    @params = Array.new
    @uri = URI.parse uri
    instance_eval &block if block

  def multipart_text key, value
    content = "Content-Disposition: form-data; name=\"#{key}\"" <<
      EOL << EOL << "#{value}" << EOL

  def multipart_file key, filename, mime_type, content
    content = "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"#{EOL}" <<
      "Content-Type: #{mime_type}\r\n" << EOL << "#{content}" << EOL

To add sections to the @params, I will need two addition methods:

class MultipartPost
  # omited codes...

  def params_part key, value
    @params << multipart_text(key, value)

  def files_part key, filename, mime_type, content
    @params << multipart_file(key, filename, mime_type, content)

  # omited codes...

Now, I have to put all the section into one string. Remeber that each section will be separated by a boundary and terminated with boundary + "--". So, this class has to have a method to putting it altogether:

class MultipartPost
  BOUNDARY = "-----------RubyMultipartPost"

  # omited codes...
  def request_body
    body = @params.map{|p| "--#{BOUNDARY}#{EOL}" << p}.join ""
    body << "#{EOL}--#{BOUNDARY}--#{EOL}"

  # omited codes...

Now, I have everything I need to create a multipart post request string for upload files. The only left is issue the request to a web server. This is the job for ruby Net:HTTP library. The MultipartPost class need a method that use Net::HTTP library to issuse the request, like so:

def run
  http = Net::HTTP.new @uri.host, @uri.port
  request = Net::HTTP::Post.new @uri.request_uri
  request.body = request_body
  request.set_content_type "multipart/form-data", {"boundary" => BOUNDARY}
  res = http.request request

Using this class is very simple. All you need to do is create a new MultipartPost class instance with an url string and passed it a block that constructing the section, and then call run() method on that instance.

multi_part = MultipartPost.new post_url do
  params_part "key", value
  files_part "file-key", filename,
    file_content_type, file-data

My full source code here

All Rights Reserved

Let's register a Viblo Account to get more interesting posts.