89

Trying to use fileReader.readAsBinaryString to upload a PNG file to the server via AJAX, stripped down code (fileObject is the object containing info on my file);

var fileReader = new FileReader();

fileReader.onload = function(e) {
    var xmlHttpRequest = new XMLHttpRequest();
    //Some AJAX-y stuff - callbacks, handlers etc.
    xmlHttpRequest.open("POST", '/pushfile', true);
    var dashes = '--';
    var boundary = 'aperturephotoupload';
    var crlf = "\r\n";

    //Post with the correct MIME type (If the OS can identify one)
    if ( fileObject.type == '' ){
        filetype = 'application/octet-stream';
    } else {
        filetype = fileObject.type;
    }

    //Build a HTTP request to post the file
    var data = dashes + boundary + crlf + "Content-Disposition: form-data;" + "name=\"file\";" + "filename=\"" + unescape(encodeURIComponent(fileObject.name)) + "\"" + crlf + "Content-Type: " + filetype + crlf + crlf + e.target.result + crlf + dashes + boundary + dashes;

    xmlHttpRequest.setRequestHeader("Content-Type", "multipart/form-data;boundary=" + boundary);

    //Send the binary data
    xmlHttpRequest.send(data);
}

fileReader.readAsBinaryString(fileObject);

Examining the first few lines of a file before upload (using VI) gives me

enter image description here

The same file after upload shows

enter image description here

So it looks like a formatting/encoding issue somewhere, I tried using a simple UTF8 encode function on the raw binary data

    function utf8encode(string) {
        string = string.replace(/\r\n/g,"\n");
        var utftext = "";

        for (var n = 0; n < string.length; n++) {

            var c = string.charCodeAt(n);

            if (c < 128) {
                utftext += String.fromCharCode(c);
            }
            else if((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }

        }

        return utftext;
    )

Then in the original code

//Build a HTTP request to post the file
var data = dashes + boundary + crlf + "Content-Disposition: form-data;" + "name=\"file\";" + "filename=\"" + unescape(encodeURIComponent(file.file.name)) + "\"" + crlf + "Content-Type: " + filetype + crlf + crlf + utf8encode(e.target.result) + crlf + dashes + boundary + dashes;

which gives me the output of

enter image description here

Still not what the raw file was =(

How do I encode/load/process the file to avoid the encoding issues, so the file being received in the HTTP request is the same as the file before it was uploaded.

Some other possibly useful information, if instead of using fileReader.readAsBinaryString() I use fileObject.getAsBinary() to get the binary data, it works fine. But getAsBinary only works in Firefox. I've been testing this in Firefox and Chrome, both on Mac, getting the same result in both. The backend uploads are being handled by the NGINX Upload Module, again running on Mac. The server and client are on the same machine. The same thing is happening with any file I try to upload, I just chose PNG because it was the most obvious example.

Blank
  • 4,635
  • 5
  • 33
  • 53

3 Answers3

113

(Following is a late but complete answer)

FileReader methods support


FileReader.readAsBinaryString() is deprecated. Don't use it! It's no longer in the W3C File API working draft:

void abort();
void readAsArrayBuffer(Blob blob);
void readAsText(Blob blob, optional DOMString encoding);
void readAsDataURL(Blob blob);

NB: Note that File is a kind of extended Blob structure.

Mozilla still implements readAsBinaryString() and describes it in MDN FileApi documentation:

void abort();
void readAsArrayBuffer(in Blob blob); Requires Gecko 7.0
void readAsBinaryString(in Blob blob);
void readAsDataURL(in Blob file);
void readAsText(in Blob blob, [optional] in DOMString encoding);

The reason behind readAsBinaryString() deprecation is in my opinion the following: the standard for JavaScript strings are DOMString which only accept UTF-8 characters, NOT random binary data. So don't use readAsBinaryString(), that's not safe and ECMAScript-compliant at all.

We know that JavaScript strings are not supposed to store binary data but Mozilla in some sort can. That's dangerous in my opinion. Blob and typed arrays (ArrayBuffer and the not-yet-implemented but not necessary StringView) were invented for one purpose: allow the use of pure binary data, without UTF-8 strings restrictions.

XMLHttpRequest upload support


XMLHttpRequest.send() has the following invocations options:

void send();
void send(ArrayBuffer data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);

XMLHttpRequest.sendAsBinary() has the following invocations options:

void sendAsBinary(   in DOMString body );

sendAsBinary() is NOT a standard and may not be supported in Chrome.

Solutions


So you have several options:

  1. send() the FileReader.result of FileReader.readAsArrayBuffer ( fileObject ). It is more complicated to manipulate (you'll have to make a separate send() for it) but it's the RECOMMENDED APPROACH.
  2. send() the FileReader.result of FileReader.readAsDataURL( fileObject ). It generates useless overhead and compression latency, requires a decompression step on the server-side BUT it's easy to manipulate as a string in Javascript.
  3. Being non-standard and sendAsBinary() the FileReader.result of FileReader.readAsBinaryString( fileObject )

MDN states that:

The best way to send binary content (like in files upload) is using ArrayBuffers or Blobs in conjuncton with the send() method. However, if you want to send a stringifiable raw data, use the sendAsBinary() method instead, or the StringView (Non native) typed arrays superclass.

chris
  • 1,831
  • 18
  • 33
KrisWebDev
  • 9,342
  • 4
  • 39
  • 59
  • 12
    I'm sorry to dig this again, just wanted to add that probably the easiest way to send binary data (etc. a PDF file) is through `FileReader.readAsDataURL` and on `onload` handler instead of just sending the `event.target.result` (which is not a clean base64 encoded string) you clean it up first with some regex like `event.target.result = event.target.result.match(/,(.*)$/)[1]` and send the real base64 to server to be decoded. –  Aug 19 '14 at 11:37
  • Since anyone can edit MDN, I probably wouldn't use it as a source. – chris May 22 '15 at 17:40
  • 4
    @user1299518, better use `event.target.result.split(",", 2)[1]`, not `match`. – MrKsn Apr 06 '16 at 17:12
  • 1
    @KrisWebDev: In the recommended option you mention the need to make a separate send(). Why? – Readren Apr 10 '17 at 08:53
  • The recommended approach worked for uploading an attachment using the TFS REST API. Thx! – RoJaIt Jan 07 '19 at 16:10
77

Use fileReader.readAsDataURL( fileObject ), this will encode it to base64, which you can safely upload to your server.

c69
  • 19,951
  • 7
  • 52
  • 82
  • 8
    While that works, the version of the file saved on the server is Base64 encoded (as it should be). Is there no way to transfer it as binary data rather than Base64 encoded (I.E. as though it had been uploaded using a normal `` field) – Blank Sep 15 '11 at 13:20
  • 2
    If you have PHP on the server you can base64_decode(file) before storing it. And no - there is no safe way to transfer raw binary data over http. – c69 Sep 15 '11 at 13:24
  • Using readAsDataURL gives me this http://imgur.com/1LHya on the server, running it back through PHP's base64_decode (We're actually using Python, but PHP is a good test) I get http://imgur.com/0uwhy, still not the original binary data and not a valid image =( – Blank Sep 15 '11 at 13:32
  • 21
    @http://imgur.com/1LHya O, my bad! At the server, you must split the base64 string by "," and store only the second part - so mime type will not get stored with actual file content. – c69 Sep 15 '11 at 13:39
  • Is it efficient to do so? – Subin Jacob Aug 19 '13 at 09:31
  • 7
    no it is not efficient. this will increase file size 137% and make server overhead. but there is no other way to support F*** IE – puchu Dec 24 '13 at 15:43
  • If you have PHP? Are you sure? It's really easy to decode base64 so your comment is inaccurate. – Iharob Al Asimi Jun 05 '16 at 04:15
26

The best way in browsers that support it, is to send the file as a Blob, or using FormData if you want a multipart form. You do not need a FileReader for that. This is both simpler and more efficient than trying to read the data.

If you specifically want to send it as multipart/form-data, you can use a FormData object:

var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("POST", '/pushfile', true);
var formData = new FormData();
// This should automatically set the file name and type.
formData.append("file", file);
// Sending FormData automatically sets the Content-Type header to multipart/form-data
xmlHttpRequest.send(formData);

You can also send the data directly, instead of using multipart/form-data. See the documentation. Of course, this will need a server-side change as well.

// file is an instance of File, e.g. from a file input.
var xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("POST", '/pushfile', true);

xmlHttpRequest.setRequestHeader("Content-Type", file.type);

// Send the binary data.
// Since a File is a Blob, we can send it directly.
xmlHttpRequest.send(file);

For browser support, see: http://caniuse.com/#feat=xhr2 (most browsers, including IE 10+).

Ralf
  • 14,655
  • 9
  • 48
  • 58
  • xmlHttpRequest.send(formData); – Li-chih Wu Nov 25 '14 at 09:24
  • 7
    Finally a proper answer also without using `FormData`. It seems everyone is using a form while all they need is upload a single file... Thanks! – Wilt Jan 29 '15 at 10:08
  • I was looking for hours how to get this working for an mp3 file upload via ajax, this does the trick! – Justin Vincent Apr 23 '19 at 05:29
  • One thing I think you may not need to do setRequestHeader since it will be auto set by sending formData and will look something like this "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQA8d7glpaso6zKsA" In my case it broke CORS unless I removed setRequestHeader. – Justin Vincent Apr 23 '19 at 05:36
  • Note: My comment above only applies when using formData object. – Justin Vincent Apr 23 '19 at 05:56
  • I always receive additional data (`------WebKitFormBoundaryDFkcJjFCMw2wRQX9..Content-Disposition:...`) no matter i use formdata or not. How not to receive this additional data? – bjan Nov 18 '21 at 09:43