Retrieve a document utilizing XHR on iOS Safari platform

I'm currently working on adding the capability to download a file stored on a server. To access the file, I need to include the Authorization header, requiring me to send an XHR request to retrieve the file from the server. Since the file content is stored in a variable, I have to create a data URL to set it as the href attribute of an anchor tag and simulate a click event to initiate the file download programmatically. The functionality works well in most browsers (with a separate code for IE11), but I am encountering errors specifically in iOS Safari versions. Below is the code snippet I am using -


var isBrowserIE = window.navigator && window.navigator.msSaveOrOpenBlob;
var dataHref = 'https://example.com/doc.pdf';
var xhr = new XMLHttpRequest();
xhr.open('GET', dataHref, true);
xhr.setRequestHeader('Content-Type', 'application/pdf');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.responseType = isBrowserIE ? 'blob' : 'arraybuffer';
xhr.onload = function (e) {
    if (this.status == 200) {
        //For IE11
        if (isBrowserIE) {
            var blob = new Blob([this.response], { type: 'application/pdf' });

            var bool = window.navigator.msSaveOrOpenBlob(blob, docName);
            if (!bool) {
                alert("Download failed, Please try again later");
            }
        } else {
            var uInt8Array = new Uint8Array(this.response);
            var i = uInt8Array.length;
            var binaryString = new Array(i);
            while (i--) {
                binaryString[i] = String.fromCharCode(uInt8Array[i]);
            }
            var data = binaryString.join('');

            var base64 = window.btoa(data);

            var dataUrl = 'data:application/octet-stream;charset=utf-16le;base64,' + base64;
            var element = document.createElement('a');
            element.setAttribute('href', dataUrl);
            element.setAttribute('download', 'doc.pdf');
            element.style.display = 'none';
            document.body.appendChild(element);
            element.click();
            document.body.removeChild(element);
        }
    } else {
        alert("Download failed, Please try again later");
        closeWindow();
    }
};

xhr.send();

The error I'm facing is related to Safari and reads -

Safari cannot open the page.<br><br>The error was: “Data URL decoding&nbsp;failed”.

I'm unsure what may be causing this error. It seems to only occur on iPad 4 and iPad 5, while still working on iPad mini and iPhone XR devices. The inconsistency across different iOS versions is puzzling.

Answer №1

After much deliberation, I've finally cracked the code! Below is my finalized script along with detailed explanations in the comments section (Apologies for sticking to ES5 code as the project doesn't use Babel and has to support IE11) -

/* exported DownloadHandler */
/* global Uint8Array*/
var DownloadHandler = (function() {
  function isMobileDevice() {
    return navigator.userAgent.match(/Android|iPhone|iPad|iPod|BlackBerry|Opera Mini|IEMobile/i);
  }
  function isChromeBrowser() {
    return navigator.userAgent.match(/Crios|Chrome/i);
  }
  function isIEBrowser() {
    return window.navigator && window.navigator.msSaveOrOpenBlob;
  }
  function isSafariBrowser() {
    return navigator.userAgent.match(/Safari/i);
  }
  function getResponseType() {
    // Both Desktop Chrome and IE supports blob properly
    // Chrome also supports Data URI way, but it fails miserably when the file size is more than 2 MB (Not sure about the exact limit though).
    if (isIEBrowser() || isChromeBrowser()) {
      return 'blob';
    } else if (isMobileDevice()) {
      return 'arraybuffer';
    }
    return 'blob';
  }
  function getBlobUriFromResponse(response) {
    var blob = new Blob([response], { type: 'application/pdf' });
    var downloadUrl = URL.createObjectURL(blob);
    return downloadUrl;
  }

  function getDataUriFromResponse(response) {
    var uInt8Array = new Uint8Array(response);
    var i = uInt8Array.length;
    var binaryString = new Array(i);
    while (i--) {
      binaryString[i] = String.fromCharCode(uInt8Array[i]);
    }
    var data = binaryString.join('');

    var base64 = window.btoa(data);

    var dataUrl = 'data:application/octet-stream;charset=utf-16le;base64,' + base64;
    return dataUrl;
  }
  function downloadFileUsingXHR(fileName, fileUrl, fileMimeType, requestType, headersList) {
    var xhr = new XMLHttpRequest();
    xhr.open(requestType, fileUrl, true);
    xhr.setRequestHeader('Content-Type', fileMimeType);
    for (var i = 0; i < headersList.length; i++) {
      var header = headersList[i];
      xhr.setRequestHeader(header.key, header.value);
    }
    xhr.responseType = getResponseType();
    xhr.onload = function() {
      if (this.status == 200) {
        //For IE11
        //IE uses blob with vendor specific code
        if (isIEBrowser()) {
          // Create a new Blob object using the response data of the onload object
          var blob = new Blob([this.response], { type: fileMimeType });

          var bool = window.navigator.msSaveOrOpenBlob(blob, fileName);
          if (!bool) {
            alert('Download failed, Please try again later');
          }
        } else {
          var dataUrl;
          if (this.responseType === 'blob') {
            dataUrl = getBlobUriFromResponse(this.response);
          } else {
            dataUrl = getDataUriFromResponse(this.response);
          }
          var element = document.createElement('a');
          // Safari doesn't work well with blank targets
          if (!isSafariBrowser()) {
            element.setAttribute('target', '_blank');
          }
          element.setAttribute('href', dataUrl);
          element.setAttribute('download', fileName);
          element.style.display = 'none';
          document.body.appendChild(element);
          element.click();
          document.body.removeChild(element);
        }
      } else {
        alert('Download failed, Please try again later');
      }
    };

    xhr.send();
  }
  return {
    downloadFileUsingXHR: downloadFileUsingXHR
  };
})();

Now, here's how you can implement the aforementioned code:

DownloadHandler.downloadFileUsingXHR('example.pdf', 'https://example.com/doc.pdf', 'application/pdf','GET',[{key:'Authorization',value:'Bearer ' + token}]);

In the future, I might convert this into a library and provide a link for easy access. This will also give me an opportunity to refine the code further.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Should we define all public methods or prototype each method separately?

Typically, when I create a library, my approach is as follows: var myLibrary = (function() { return { publicProperty: 'test', publicMethod: function() { console.log('public function'); }, ...

What benefits does using Object.create(null) offer in comparison to utilizing a Map?

I've noticed that some developers prefer using code that looks like const STUFF_MAP = Object.create(null); It seems that STUFF_MAP is intended to be used like a map, hence the choice of using Object.create(null) over {} (to avoid conflicts with prope ...

Can you build and run a production-ready Vue application on your local system?

After running the npm run build command in my vue project, I placed the dist folder in C:\xampp\htdocs\ and launched the apache server to test the app on my local machine. However, when I try to access http://localhost/dist/index.html in my ...

Does the Error page in Next JS 13 fail to properly manage fetch API errors?

After referencing the documentation for Next.js with app router, I followed the instructions to create an error.tsx page within my app folder (app/[lang]/error.tsx). This file contains the code provided in the docs. Additionally, I included some API calls ...

Is it possible that Typescript does not use type-guard to check for undefined when verifying the truthiness of a variable?

class Base {} function log(arg: number) { console.log(arg); } function fn<T extends typeof Base>( instance: Partial<InstanceType<T>>, key: keyof InstanceType<T>, ) { const val = instance[key]; if (val) { ...

Having received a status code of 200 in Flask WebApp, I am still unable to access the page

Currently in the process of developing a webapp using Flask, I have created the homepage where users are required to input their phone number and password. Once entered, the system will send an OTP to the provided phone number. When clicking the signup bu ...

Determine the employees' salaries and display any salaries that are below 5000

Hi everyone, I'm looking for guidance on how to properly utilize the Flat or flatMap method to flatten this array of employee data. Specifically, I want to retrieve the names and salaries of employees whose salary is less than 5000. const employeeData ...

Obtain array buffer from NodeJS to capture frames from a video file

My current goal is to extract frames from a video in node without relying on a video tag. I am utilizing openvg-canvas for this task, which allows me to print images and use the canvas API functionalities successfully. The challenge lies in the fact that ...

Which codec strings are considered valid for the web codecs API?

I am searching for a definitive list of valid strings that can be passed as the `codec` parameter in `VideoEncoder.isConfigSupported({ codec, width, height })`, but I have been unable to find a clear answer so far: The TS declaration file states that it m ...

Arranging numerous Text elements within a solitary Drag and Drop container with the z-index property

I am facing a challenge with stacking twelve arguments in no particular order on a drag and drop element. The texts overlap each other, making it difficult for the end user to see them clearly when dragging and dropping. Is there a way to stack texts using ...

React's Conditional Rendering

Let's imagine having these initial conditions: this.state = {plans: [], phase: 'daybreak'} along with a dynamic JSON object fetched from an API containing various schedules that may change periodically, for example: plans = {"daybreak": " ...

Creating Pop-up Dialog Boxes in WebForms Using jQuery

I have implemented a Plugin in Webforms using jQuery UI dialog. Since I have a Content Place holder on my Form, I had to modify the code as shown below: <asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server"> <link rel="stylesheet" ...

Combining Protractor, CucumberJS, and Gulp-Protractor results in the browser not closing when a test fails

Hello! I am facing an issue with closing the browser when a test fails. Currently, the browser closes successfully when the test passes. The dependencies I am using are: "cucumber": "^0.9.2", "gulp": "~3.9.0", "gulp-protractor": "^2.1.0", "protractor": ...

Tips for assigning unique names to each radio input groupNeed to assign unique names to radio

Currently, I am seeking a dynamic solution to alter the name associated with a set of radio input buttons. The situation involves creating a travel itinerary where users can choose between "domestic" and "international." Based on this selection, the corre ...

Executing a function in AngularJS using PHP

My current project involves an Angular application that needs to load a PHP page within the view. The setup is functioning properly, but I now require an Angular function to run after the PHP code has loaded. I am wondering about the process of calling an ...

Trouble with $http response not appearing in AngularJS application

Currently, I am in the process of developing an angularjs application and encountering a challenging issue with setting up the $http connection to a php file. The header is displaying a response that I echoed from php. Nevertheless, two key problems persis ...

Is there a way to adjust the state value in Pinia within a Vue3 component test, and have an impact on the component?

When testing the component using pinia with vue-test-utils, I encountered difficulty in modifying the state value stored in pinia. Despite trying multiple methods, I was unable to achieve the desired result. The original component and store files are provi ...

Instructions on creating an input field and a slider simultaneously

Currently, I am exploring the world of CSS 3D transforms and I am particularly interested in creating sliders to manage these transforms. I recently stumbled upon the Three.js editor and was impressed by how it handles the positioning, rotation, and scalin ...

What is the method for accessing cookies using JSONP?

Hello, I am trying to load a page from index.html using the following code: <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.12.2.min.js"></script> <script> function jsonCallback(json){ console.log(json); alert(document.c ...

Sending arguments to browser.executeScript

How can I provide parameters to browser.executeScript static sortableDragAndDropByClassName(dragElementClassName: string, dropElementClassName: string) { return browser.executeScript(function () { console.log(dragElementClassName); conso ...