Binary multipart POSTs in Javascript
We recently released a very slick Firefox extension at Wesabe. It was written by my colleague Tim Mason, but I helped figure out one small piece of it—namely, how to do binary multipart POSTs in Javascript—and since it involved many hours of hair-pulling for both of us, I thought I’d share the knowledge. (Tim says I should note that “this probably only works in Firefox version 2.0 and greater _and_ that it uses Mozilla specific calls that are only allowed for privileged javascript—basically only for extensions.”)
One of the cool features of the plugin is the ability to take a snapshot of a full browser page and either save the snapshot to disk or upload it to Wesabe (so you can, for example, save the receipt for a web purchase along with that transaction in your account). The snapshot is uploaded to Wesabe via a standard multipart POST, the same way that a file is uploaded via a web form.
Tim was having trouble getting the POST to work with binary data at first, and he had other things to finish, so he wanted to just base-64-encode it and be done with it. I was reluctant to do that, as the size of the upload would be significantly larger (about 137% of the original). Also, Rails didn’t automatically decode base-64-encoded file attachments. But Tim had other bugs to fix, so I submitted a patch to Rails to do the base-64 decoding. I was pretty proud of this patch until it was pointed out to me that RFC 2616 specifically disallows the use of Content-Transfer-Encoding in HTTP. Doh. They also realized that it is a colossal waste of bandwidth.
Since Tim was cramming to meet a hard(-ish) deadline set for the release of the plugin, I offered to lend my eyeballs to the binary post problem. This could be a very long story, but I’ll just get to the point: you can read binary data in to a Javascript string and dump it right out to a file just fine, but if you try to do any concatenation with that string, Javascript ends up munging it mercilessly. I’m not sure whether it is trying to interpret it as UTF8 or if it terminates it as soon as it hits a null byte (which is what seemed to be happening), but regardless, doing "some string" + binaryData + "another string", as is necessary when putting together a mutipart post, just does not work.
The answer required employing Rube Goldbergian system of input and output streams. The seed of the solution was found on this post, although that didn’t explain how to mix in all of the strings needed for the post and MIME envelope. So here it is, in all it’s goriness:
var dataURL = this.canvas.toDataURL(this.getImageType()); // grab the snapshot as base64
var imgData = atob(dataURL.substring(13 + this.getImageType().length)); // convert to binary
var filenameTimestamp = (new Date().getTime());
var separator = "----------12345-multipart-boundary-" + filenameTimestamp;
// Javascript munges binary data when it undergoes string operations (such as concatenation), so we need
// to jump through a bunch of hoops with streams to make sure that doesn't happen
// create a string input stream with the form preamble
var prefixStringInputStream = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
var formData =
"--" + separator + "\\r\\n" +
"Content-Disposition: form-data; name=\"data\"; filename=\"snapshot_" + filenameTimestamp +
(this.getImageType() === "image/jpeg" ? ".jpg" : ".png") + "\"\\r\\n" +
"Content-Type: " + this.getImageType() + "\\r\\n\\r\\n";
prefixStringInputStream.setData(formData, formData.length);
// write the image data via a binary output stream, to a storage stream
var binaryOutputStream = Components.classes["@mozilla.org/binaryoutputstream;1"].createInstance(Components.interfaces.nsIBinaryOutputStream);
var storageStream = Components.classes["@mozilla.org/storagestream;1"].createInstance(Components.interfaces.nsIStorageStream);
storageStream.init(4096, imgData.length, null);
binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));
binaryOutputStream.writeBytes(imgData, imgData.length);
binaryOutputStream.close();
// write out the rest of the form to another string input stream
var suffixStringInputStream = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
formData =
"\\r\\n--" + separator + "\\r\\n" +
"Content-Disposition: form-data; name=\"description\"\\r\\n\\r\\n" + description + "\\r\\n" +
"--" + separator + "--\\r\\n";
suffixStringInputStream.setData(formData, formData.length);
// multiplex the streams together
var multiStream = Components.classes["@mozilla.org/io/multiplex-input-stream;1"].createInstance(Components.interfaces.nsIMultiplexInputStream);
multiStream.appendStream(prefixStringInputStream);
multiStream.appendStream(storageStream.newInputStream(0));
multiStream.appendStream(suffixStringInputStream);
// post it
req.open("POST", "http://yoursite.com/upload_endpoint", true);
req.setRequestHeader("Accept", "*/*, application/xml");
req.setRequestHeader("Content-type", "multipart/form-data; boundary=" + separator);
req.setRequestHeader("Content-length", multiStream.available());
req.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
req.setRequestHeader("User-Agent", "YourUserAgent/1.0.0");
req.send(multiStream);
Update: Spaces removed from multipart boundary per Gijsbert’s suggestion in the comments (thanks!).
September 14th, 2007 at 1:45 pm
Thanks so much for the article. I have been searching the internet for two days, about how to upload a binary file. I almost gave up and about to use base 64 encoding when I found this.
February 6th, 2008 at 1:22 pm
That’s pretty interesting. I’m trying to do a similar thing. We use Webrenderer to let the user browse websites and then snapshot the website when they find a website with important information. The unfortunate part is that I want to send this down on a multipart form, but those pesky javascript security violations get thrown. Oh well. What I am gonna end up doing is converting my binary data to a comma delimited string that represents the bytes, send that down on the form, and then convert it back again. This will ‘bloat’ my file, but I can’t figure out what else I could do.
February 8th, 2008 at 12:10 am
Wow, i was struggling with the same problem for a couple of days. It works like a charm now. Thanks a lot.
March 3rd, 2008 at 1:35 pm
Excellent Work. Thank you very much for sharing.
April 8th, 2008 at 8:30 pm
Do you know of any way this can be done using vanilla javascript (plus, if needed, javascript libraries like jquery for example) without resorting to mozilla specific classes?
Thanx in advance for your help
April 8th, 2008 at 9:12 pm
Edoardo – Tim confirms that it is not possible without the Mozilla code. Sorry!
May 31st, 2008 at 6:20 pm
Hi! I’m very interested how you create a binary out of the image.
It’s like this: I try currently to download an image via XMLHttpRequest with MimeType “image/jpg” and then I try to save it on my harddisk.
Sadly either the image gets corrupted in the httprequest or maybe when I save it.
Reading your blog here I now think it’s more likely that it’s corrupted while I try to save it.
If you are still in touch with this topic I would be very happy if you could please take a look at the code i posted as example on the following forum:
http://www.xulplanet.com/forum2/viewtopic.php?t=3512&sid=e66ed3d85c2026e82b36fecdbf307546
It’s the last part I have to succeed in of a small application I wrote. =)
Greetings
Dieter
(My email is very much spammed at, but I will surely come back here and on the xul-forum to see if there’s an answer.)
October 8th, 2008 at 10:20 pm
I am not getting how this script works. if i want get image from our hard disk and in firefox 3 “document.getElementById(‘Image’)”
won give me the path of it and how can i this script helps me.
October 8th, 2008 at 10:51 pm
Even am facing the same probelm .how this helps me to upload an image .coz it wont give full the path of image when we use document.getElementById method.
November 13th, 2008 at 10:54 am
Hi,
I’ve used your example code to push binary data to my site, I am very happy that I don’t have to use base64 anymore.
There was one problem, the boundary with spaces is not standard since it is not quoted in the Content-type header. For most servers this is not a problem but it was for Google Apps.
Just wanted to let you know, and hope you can update the example so others don’t run into problems.
Thanks a lot!
January 16th, 2009 at 6:21 am
Thanks a lot!
Really usefull piece of code.
July 7th, 2009 at 1:26 am
Oh, my god!
Finally I found the answer!
I have searched the Internet for this issue thousands of times, but in vain. This page is just like a light in the darkness. Thanks a lot! 谢谢(thanks in Chinese)!
May 10th, 2010 at 3:21 pm
Great stuff.
But is there any way to do this in all browsers without requiring a FF plugin?