1

I have already passed through some of the suggested solutions here in SO, but with no success. This is the problem. When the blob is generated using the data retrieved from the API endpoint, I want to force browser to download the blob. I have tried three solutions so far, but no of them works. Code follows. Notice that I put some comments in the code to explain further.

const FILE_NAME_REGEX = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
    
    export function Download(url) {
     return APICall.get(url).then(response => {
      const disposition = response.request.getResponseHeader('Content-Disposition');
      //^This line gives the 'Refused to get unsafe header "Content-Disposition"' error, so next few lines won't execute and part with generating anchor is not used, but the part of code under the 'else' branch.
    
      if (disposition && disposition.indexOf('attachment') !== -1) {
       const matches = FILE_NAME_REGEX.exec(disposition);
    
       if (matches != null && matches[1]) {
        filename = matches[1].replace(/['"]/g, '');
       }
      }
    
      const type = response.request.getResponseHeader('Content-Type');
    
      const blob = new Blob([response.data], { type });
    
      if (typeof window.navigator.msSaveBlob !== 'undefined') {
       window.navigator.msSaveBlob(blob, filename);
      } else {
       const URL = window.URL || window.webkitURL;
       const downloadUrl = URL.createObjectURL(blob);
    
       if (filename) {
        const a = document.createElement('a');
    
        if (typeof a.download === 'undefined') {
         window.location = downloadUrl;
        } else {
         a.href = downloadUrl;
         a.download = filename;
         document.body.appendChild(a);
         a.click();
         document.body.removeChild(a);
        }
       } else {

 // 1. soultion 
   window.location = downloadUrl;

 }
    
       setTimeout(() => {
        URL.revokeObjectURL(downloadUrl);
       }, 100);
      }
     });
    }

// 2. solution

        const ifrmDownloader = document.createElement('iframe');
        ifrmDownloader.setAttribute('src', downloadUrl);
        ifrmDownloader.style.width = '0px';
        ifrmDownloader.style.height = '0px';
        document.body.appendChild(ifrmDownloader);

// 3. solution

window.open(downloadUrl,_blank);
  1. Solution does not work because it opens another tab and returns empty little square instead of the file. This is probably due to the lost temp blob when new tab got opened.

  2. Solution simply does not work, and I do not know why. Iframe gets added to the dom and request is recorded in the Network tab under developer console, but no download is trigerred. Probably the same reason as in the solution 1. In addition this message is logged:

Resource interpreted as Document but transferred with MIME type image/png: "blob:https://example.com/e53bf47b-7f39-4758-b3dd-3cc2df5889ad".

  1. Solution does not work as well. First of, browser blocked pop up, and after te pop up is allowed, no file download is initiated.

I feel that something is missing here, but what?

2 Answers2

2

Your code is set up to download the File only when the filename variable is set.

This variable is set only when you are able to read the Content-Disposition header (i.e only when same-origin).

So the simple fix would be to set a dummy value for filename when you failed to get it from the Headers, this way, filename is always set, and you always try to download it insted of just opening it in a new page.
The smarter fix would be to try to parse the filename from the url variable, but without knowing the format of possible urls, it's hard to make one for you.

const FILE_NAME_REGEX = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;

export function Download(url) {
  return APICall.get(url).then(response => {
      const disposition = response.request.getResponseHeader('Content-Disposition');
      if (disposition && disposition.indexOf('attachment') !== -1) {
        const matches = FILE_NAME_REGEX.exec(disposition);

        if (matches != null && matches[1]) {
          filename = matches[1].replace(/['"]/g, '');
        }
      }
      if (!filename) {
        // getFileNameFromURL(url);
        filename = 'dummy.png';
      }
      const type = response.request.getResponseHeader('Content-Type');

      const blob = new Blob([response.data], {
        type
      });

      if (typeof window.navigator.msSaveBlob !== 'undefined') {
        window.navigator.msSaveBlob(blob, filename);
      } else {
        const URL = window.URL || window.webkitURL;
        const downloadUrl = URL.createObjectURL(blob);

//     if (filename) { // always truthy
        const a = document.createElement('a');

        if (typeof a.download === 'undefined') {
          window.location = downloadUrl;
        } else {
          a.href = downloadUrl;
          a.download = filename;
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
        }
//    }
/*  else { // useless
    window.location = downloadUrl;
    }
*/
      setTimeout(() => {
        URL.revokeObjectURL(downloadUrl);
      }, 100);
    }
  });
}
Kaiido
  • 123,334
  • 13
  • 219
  • 285
0

Here is what I have found in regards to the problem I posted. There were two problems actually:

  1. Content-Disposition header was getting refused and therefore no filename nor the dispostion information were available. This was bridged according to @Kaiido's reply, by extracting the filename from the url and adding ',png'. E.G.

    const chunks = extendedURl.split('/');
    const pngExtension = '.png';
    const baseName = chunks[chunks.length - 1].split('?')[0];
    filename = `${baseName}${pngExtension}`;
    
  2. More serious problem that I did not realize until today was that when axios' Get method was called, responseType was not set to 'arraybuffer'.

    Therefore, I was able to download files that were 'broken'. This question helped me figure out where the actual problem was: File is corrupted while downloading using angular $http

Once the calling Get method was provided with the 'responseType: "arraybuffer"', files started to appear normal, not 'broken'.

To sum it up, when calling .net Core Web API endpoint that returns FileStreamResult, in order to be able to create Blob in your FE JavaScript, you should explicitly set responseType to 'arraybuffer' as follows:

axios.get(extendedURl, { responseType: 'arraybuffer' }).then(response =>...