C++ REST SDK (Casablanca) Multipart form data HTTP request parser

Tags: , ,

Objective

We want to parse an http request for working with multipart/form data in a C++ REST SDK (Casablanca) project.

The problem

There have been questions about how to upload an image file using C++ REST SDK (multipart form POST example, http_listener and multipart/form-data, Multipart request handling in the server side using http listener)

But for the moment there is no integrated multipart parsing support. That is why we developed our own simple parser. I'm going to explain how it works so you can download and use it. Feel free to modify it, comment and ask if you have problems.

Download Parser

parser.h: granada::http::parser header
parser.cc: granada::http::parser cc

Let's cook

What we are going to do is send the data of a form containing an image file via AJAX to our REST server via an HTTP POST request. Then in the server side we are going to parse the http_request, save our image file in our hard drive and finally respond to the client.

Client side

We use an HTML form with a file field and another text field as an example:

<form enctype="multipart/form-data">
	<input type="text" name="test-text-field" value="test value" />
	<input type="file" name="file"/>
  <button type="button" id="upload-button">Upload</button>
</form>

We send the form, it can be done via AJAX. To simplify, here is the code using jQuery:

$('#upload-button').click(function(){
	var formData = new FormData($('form')[0]);
	$.ajax({
		url: "/test",
		type: "POST",
		success: function(data){
			console.log('data => ', data);
		},
		data: formData,
		cache: false,
		contentType: false,
	  processData: false
  });
});

Server side

Here is how we parse the data in a post handler function. The function receives an http_request containing our file field and our text field we just sent, parses it into an unordered_map, then extracts the file from our parsed data, saves the file in the hard drive and responds with a json to the client. For that purpose we will use the http_request parser (parser.h and parser.cc):

void TestController::handle_post(http_request request){

      // parse multipart/form data content of http request
      std::unordered_map<std::string, std::unordered_map<std::string, std::vector<unsigned char>>> multipart_form_data = granada::http::parser::ParseMultipartFormData(request);

      // extract file field from parsed data
      std::unordered_map<std::string, std::vector<unsigned char>> file_properties = multipart_form_data["file"];
      std::vector<unsigned char> file_name_and_extension = file_properties["filename"];
      std::string file_name_and_extension_str(file_name_and_extension.begin(),file_name_and_extension.end());

      // build file path
      std::string file_path = "/path/to/images/directory/" + file_name_and_extension_str;

      // file where image is going to be.
      std::ofstream FILE(file_path, std::ios::out | std::ofstream::binary);

      // extract file data and save it into file.
      std::vector<unsigned char> file_vector = file_properties["value"];
      std::copy(file_vector.begin(), file_vector.end(), std::ostreambuf_iterator(FILE));

      // response with the path of the saved file.
      request.reply(status_codes::OK, U("{\"success\":true,\"message\":\"File uploaded.\",\"data\":{\"src\":\"" + file_path + "\"}}"), "text/json; charset=utf-8");
}
That's it!

How the parser works

Example of an HTTP request:

Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 6500
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymBItcSVphgAjmbJC
Host: localhost
Origin: http://localhost
Pragma: no-cache
Referer: http://localhost/?file=test-image.png
User-Agent: Mozilla/5.0 (X11; Linux x86_64)(...)
X-Requested-With: XMLHttpRequest
------WebKitFormBoundarymBItcSVphgAjmbJC
Content-Disposition: form-data; name="test-text-field"

Test value
------WebKitFormBoundarymBItcSVphgAjmbJC
Content-Disposition: form-data; name="file"; filename="test-image.png"
Content-Type: image/png

\89PNG
#
\00\00\00
IHDR\00\00\00d\00\00\00^##\00\00\00\D5#\B1\9F\00\00\00#
(…)
------WebKitFormBoundarymBItcSVphgAjmbJC--
What parser does:
1- Get boundary that separates each block of fields from headers so we can fragment the request body:
WebKitFormBoundarymBItcSVphgAjmbJC
2- Remove headers and work only with the body of the HTTP request:
------WebKitFormBoundarymBItcSVphgAjmbJC
Content-Disposition: form-data; name="test-text-field"

Test value
------WebKitFormBoundarymBItcSVphgAjmbJC
Content-Disposition: form-data; name="file"; filename="test-image.png"
Content-Type: image/png

\89PNG
#
\00\00\00
IHDR\00\00\00d\00\00\00^##\00\00\00\D5#\B1\9F\00\00\00#
(…)
------WebKitFormBoundarymBItcSVphgAjmbJC--
3- Parse each block of fields: extract the properties of the field and its value and insert them into an unordered_map of string and vector of unsigned char.Then Insert the unordered_map of properties into the multipart_form_data unordered_map that contains all the fields:
parsed multipart form (unordered_map)
	|_ test-text-field (unordered_map)
	|	|_ value (vector)
	|
	|_ file (unordered_map)
		|_ value (vector)
		|_ filename (vector)
		|_ Content-Type (vector)

Helpful links

Classes References

http_request: web::http::http_request Class Reference
http_headers: web::http::http_headers Class Reference
http_response: web::http::http_response Class Reference

More

Hypertext Transfer Protocol – HTTP/1.1 W3
using HTTP POST to upload Image
How to set http_requset body in an HTTP POST with content type "multipart/form-data"