Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how do I access XHR responseBody (for binary data) from Javascript in IE?

Tags:

I've got a web page that uses XMLHttpRequest to download a binary resource.

In Firefox and Gecko I can use responseText to get the bytes, even if the bytestream includes binary zeroes. I may need to coerce the mimetype with overrideMimeType() to make that happen. In IE, though, responseText doesn't work, because it appears to terminate at the first zero. If you read 100,000 bytes, and byte 7 is a binary zero, you will be able to access only 7 bytes. IE's XMLHttpRequest exposes a responseBody property to access the bytes. I've seen a few posts suggesting that it's impossible to access this property in any meaningful way directly from Javascript. This sounds crazy to me.

xhr.responseBody is accessible from VBScript, so the obvious workaround is to define a method in VBScript in the webpage, and then call that method from Javascript. See jsdap for one example. EDIT: DO NOT USE THIS VBScript!!

var IE_HACK = (/msie/i.test(navigator.userAgent) &&                 !/opera/i.test(navigator.userAgent));     // no no no!  Don't do this!  if (IE_HACK) document.write('<script type="text/vbscript">\n\      Function BinaryToArray(Binary)\n\          Dim i\n\          ReDim byteArray(LenB(Binary))\n\          For i = 1 To LenB(Binary)\n\              byteArray(i-1) = AscB(MidB(Binary, i, 1))\n\          Next\n\          BinaryToArray = byteArray\n\      End Function\n\ </script>');   var xml = (window.XMLHttpRequest)      ? new XMLHttpRequest()      // Mozilla/Safari/IE7+     : (window.ActiveXObject)        ? new ActiveXObject("MSXML2.XMLHTTP")  // IE6       : null;  // Commodore 64?   xml.open("GET", url, true); if (xml.overrideMimeType) {     xml.overrideMimeType('text/plain; charset=x-user-defined'); } else {     xml.setRequestHeader('Accept-Charset', 'x-user-defined'); }  xml.onreadystatechange = function() {     if (xml.readyState == 4) {         if (!binary) {             callback(xml.responseText);         } else if (IE_HACK) {             // call a VBScript method to copy every single byte             callback(BinaryToArray(xml.responseBody).toArray());         } else {             callback(getBuffer(xml.responseText));         }     } }; xml.send(''); 

Is this really true? The best way? copying every byte? For a large binary stream that's not going to be very efficient.

There is also a possible technique using ADODB.Stream, which is a COM equivalent of a MemoryStream. See here for an example. It does not require VBScript but does require a separate COM object.

if (typeof (ActiveXObject) != "undefined" && typeof (httpRequest.responseBody) != "undefined") {     // Convert httpRequest.responseBody byte stream to shift_jis encoded string     var stream = new ActiveXObject("ADODB.Stream");     stream.Type = 1; // adTypeBinary     stream.Open ();     stream.Write (httpRequest.responseBody);     stream.Position = 0;     stream.Type = 1; // adTypeBinary;     stream.Read....          /// ???? what here } 

But that's not going to work well - ADODB.Stream is disabled on most machines these days.


In The IE8 developer tools - the IE equivalent of Firebug - I can see the responseBody is an array of bytes and I can even see the bytes themselves. The data is right there. I don't understand why I can't get to it.

Is it possible for me to read it with responseText?

hints? (other than defining a VBScript method)

like image 540
Cheeso Avatar asked Dec 17 '09 07:12

Cheeso


People also ask

How XMLHttpRequest works?

XMLHttpRequest (XHR) objects are used to interact with servers. You can retrieve data from a URL without having to do a full page refresh. This enables a Web page to update just part of a page without disrupting what the user is doing. XMLHttpRequest is used heavily in AJAX programming.


2 Answers

Yes, the answer I came up with for reading binary data via XHR in IE, is to use VBScript injection. This was distasteful to me at first, but, I look at it as just one more browser dependent bit of code. (The regular XHR and responseText works fine in other browsers; you may have to coerce the mime type with XMLHttpRequest.overrideMimeType(). This isn't available on IE).

This is how I got a thing that works like responseText in IE, even for binary data. First, inject some VBScript as a one-time thing, like this:

if(/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)) {     var IEBinaryToArray_ByteStr_Script =     "<!-- IEBinaryToArray_ByteStr -->\r\n"+     "<script type='text/vbscript' language='VBScript'>\r\n"+     "Function IEBinaryToArray_ByteStr(Binary)\r\n"+     "   IEBinaryToArray_ByteStr = CStr(Binary)\r\n"+     "End Function\r\n"+     "Function IEBinaryToArray_ByteStr_Last(Binary)\r\n"+     "   Dim lastIndex\r\n"+     "   lastIndex = LenB(Binary)\r\n"+     "   if lastIndex mod 2 Then\r\n"+     "       IEBinaryToArray_ByteStr_Last = Chr( AscB( MidB( Binary, lastIndex, 1 ) ) )\r\n"+     "   Else\r\n"+     "       IEBinaryToArray_ByteStr_Last = "+'""'+"\r\n"+     "   End If\r\n"+     "End Function\r\n"+     "</script>\r\n";      // inject VBScript     document.write(IEBinaryToArray_ByteStr_Script); } 

The JS class I'm using that reads binary files exposes a single interesting method, readCharAt(i), which reads the character (a byte, really) at the i'th index. This is how I set it up:

// see doc on http://msdn.microsoft.com/en-us/library/ms535874(VS.85).aspx function getXMLHttpRequest()  {     if (window.XMLHttpRequest) {         return new window.XMLHttpRequest;     }     else {         try {             return new ActiveXObject("MSXML2.XMLHTTP");          }         catch(ex) {             return null;         }     } }  // this fn is invoked if IE function IeBinFileReaderImpl(fileURL){     this.req = getXMLHttpRequest();     this.req.open("GET", fileURL, true);     this.req.setRequestHeader("Accept-Charset", "x-user-defined");     // my helper to convert from responseBody to a "responseText" like thing     var convertResponseBodyToText = function (binary) {         var byteMapping = {};         for ( var i = 0; i < 256; i++ ) {             for ( var j = 0; j < 256; j++ ) {                 byteMapping[ String.fromCharCode( i + j * 256 ) ] =                     String.fromCharCode(i) + String.fromCharCode(j);             }         }         // call into VBScript utility fns         var rawBytes = IEBinaryToArray_ByteStr(binary);         var lastChr = IEBinaryToArray_ByteStr_Last(binary);         return rawBytes.replace(/[\s\S]/g,                                 function( match ) { return byteMapping[match]; }) + lastChr;     };      this.req.onreadystatechange = function(event){         if (that.req.readyState == 4) {             that.status = "Status: " + that.req.status;             //that.httpStatus = that.req.status;             if (that.req.status == 200) {                 // this doesn't work                 //fileContents = that.req.responseBody.toArray();                   // this doesn't work                 //fileContents = new VBArray(that.req.responseBody).toArray();                   // this works...                 var fileContents = convertResponseBodyToText(that.req.responseBody);                  fileSize = fileContents.length-1;                 if(that.fileSize < 0) throwException(_exception.FileLoadFailed);                 that.readByteAt = function(i){                     return fileContents.charCodeAt(i) & 0xff;                 };             }             if (typeof callback == "function"){ callback(that);}         }     };     this.req.send(); }  // this fn is invoked if non IE function NormalBinFileReaderImpl(fileURL){     this.req = new XMLHttpRequest();     this.req.open('GET', fileURL, true);     this.req.onreadystatechange = function(aEvt) {         if (that.req.readyState == 4) {             if(that.req.status == 200){                 var fileContents = that.req.responseText;                 fileSize = fileContents.length;                  that.readByteAt = function(i){                     return fileContents.charCodeAt(i) & 0xff;                 }                 if (typeof callback == "function"){ callback(that);}             }             else                 throwException(_exception.FileLoadFailed);         }     };     //XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com]      this.req.overrideMimeType('text/plain; charset=x-user-defined');     this.req.send(null); } 

The conversion code was provided by Miskun.

Very fast, works great.

I used this method to read and extract zip files from Javascript, and also in a class that reads and displays EPUB files in Javascript. Very reasonable performance. About half a second for a 500kb file.

like image 184
Cheeso Avatar answered Sep 19 '22 21:09

Cheeso


XMLHttpRequest.responseBody is a VBArray object containing the raw bytes. You can convert these objects to standard arrays using the toArray() function:

var data = xhr.responseBody.toArray(); 
like image 45
timrice Avatar answered Sep 23 '22 21:09

timrice