How can I download a file using window.fetch?
Asked Answered
A

12

180

If I want to download a file, what should I do in the then block below?

function downloadFile(token, fileId) {
  let url = `https://www.googleapis.com/drive/v2/files/${fileId}?alt=media`;
  return fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': token
    }
  }).then(...);
}

Note: The code is on the client-side.

Aureaaureate answered 13/9, 2015 at 2:5 Comment(4)
What about adding a download attribute to a link which has the URL https://www.googleapis.com/drive/v2/files/${fileId}?alt=mediaAlvarez
@Alvarez how do you add the token to the header of your request?Cyanamide
@Cyanamide I'm sorry, I don't understand your question. With my original comment, the solution I was suggesting was to create an <a> element with a download attribute and simulating a click on that element, all with the use of JavaScript. See Zibri's answer (which, btw, was posted way after my comment).Alvarez
the api is secure, you can't just go to the url and download, need to pass a token to the header. Something like"'Authorization":"Bearer" + <token>. In this situation how do you add the token to the header?Cyanamide
A
66

I temporarily solve this problem by using download.js and blob.

let download = require('./download.min');

...

function downloadFile(token, fileId) {
  let url = `https://www.googleapis.com/drive/v2/files/${fileId}?alt=media`;
  return fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': token
    }
  }).then(function(resp) {
    return resp.blob();
  }).then(function(blob) {
    download(blob);
  });
}

It's working for small files, but maybe not working for large files. I think I should dig Stream more.

Aureaaureate answered 13/9, 2015 at 2:54 Comment(6)
just bear in mind the current blob size limit is around 500mb for browsersGrandstand
Nice, just one thing: would it be possible to get the file name from the server response so to let the user download it with its real name?Escolar
@Escolar to do so, you should pass an object from server, which contains not only the data but also the name. Once you do that, you can deal with its field separately, but for that you should see other answers here as (as far as I understand) .blob method is specific one of object which fetch resolves withDag
You need a node plugin that can resolve require -see first line - on the client side, such as bundler.js and make the download package available.Oreopithecus
Update on blob size limit: it's 2GB for desktop in 2022Largeminded
I get: "Failed to execute 'createObjectURL' on 'URL': Overload resolution failed."Blomquist
U
168

This is more shorter and efficient, no libraries only fetch API

const url ='http://sample.example.file.doc'
const authHeader ="Bearer 6Q************" 

const options = {
  headers: {
    Authorization: authHeader
  }
};
 fetch(url, options)
  .then( res => res.blob() )
  .then( blob => {
    var file = window.URL.createObjectURL(blob);
    window.location.assign(file);
  });

This solution does not allow you to change filename for the downloaded file. The filename will be a random uuid.

Unbiased answered 28/1, 2020 at 0:4 Comment(13)
is there any way to set the file name?Residuary
Yes, you can add Content-Disposition to the Headers obj, here is the documentation developer.mozilla.org/en-US/docs/Web/HTTP/Headers/…Unbiased
@LucasMatos I added “Content-Disposition” header to options object and I do get the correct header for my file when inspecting it in network tab, but then the blob is created and the name is thrown away so I end up with a generated random name. Do you know how to pass the name to blob with your solution?Panlogism
This gist.github.com/devloco/5f779216c988438777b76e7db113d05c shows the full monty.Houdon
This only opens the file in the same tab for me (using Chrome), does not download the file.Leastwise
Hello @TobiasIngebrigtØrstad can you share your code to review deeper the issue?Unbiased
Hello Lucas, your code working good, downloading a file with random name. I've tried to add headers, but no luck at all. #67768454 . Could you please head up to this?Gigantean
@Gigantean Here is the documentation, about how to capture headers on the fetch response developer.mozilla.org/en-US/docs/Web/API/Response/headersUnbiased
@LucasMatos I tried editing your answer but the queue is full. In order to assert a name to the file being downloaded is to add an extra line: var file = new File([blob], "filename.extension"); file = window.URL.createObjectURL(file);Demoralize
@Demoralize your suggestion didn't work for me: what worked but without setting filename is const fileUrl = window.URL.createObjectURL(blob); window.location.assign(fileUrl), but when I modified it to const file = new File([blob], fileName); const fileUrl = window.URL.createObjectURL(file); window.location.assign(fileUrl), I get browser trying to display the binary on a different url (like blob:blob://http://localhost:8070/7174db61-b974-44fb-8a15-946f400378d0). Any ideas what's wrong with it?Dag
For anyone coming here, check this: stackoverflow.com/a/56923508Subsellium
It does not work! it refreshes my page.Martinic
Content-Disposition is not a safe header. So I think you have to expose it first, then read it, then manually put it in the download attribute using the append anchor way.Largeminded
C
137

EDIT: syg answer is better. Just use downloadjs library.

The answer I provided works well on Chrome, but on Firefox and IE you need some different variant of this code. It's better to use library for that.


I had similar problem (need to pass authorization header to download a file so this solution didn't helped).

But based on this answer you can use createObjectURL to make browser save a file downloaded by Fetch API.

getAuthToken()
    .then(token => {
        fetch("http://example.com/ExportExcel", {
            method: 'GET',
            headers: new Headers({
                "Authorization": "Bearer " + token
            })
        })
        .then(response => response.blob())
        .then(blob => {
            var url = window.URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            a.download = "filename.xlsx";
            document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox
            a.click();    
            a.remove();  //afterwards we remove the element again         
        });
    });
Chiaroscuro answered 16/2, 2017 at 12:32 Comment(7)
@David I updated the answer. Should now also work in FF. The problem was, that FF wants the link element to be appended to the DOM. This is not mandatory in Chrome or Safari.Ing
It makes sense to call URL.revokeObjectURL in the end to avoid a memory leak.Scourge
@AndrewSimontsev Great tip, thanks for the input! I edited the response, let me know if it is correct that way. Tested on my code and seems ok!Publicly
I disagree that using a library is a better answer :). Using external libraries is not always an option, and when it is, then finding libraries is easy. It's not answer worthy, which is likely why your answer has more votes than the accepted answer despite the accepted being 2 years older.Featherbrain
Also looking at the code on downloadjs, they use the same method of a temp <a/> to save the file. So the library doesn't necessarily do it a better way.Triglyph
This code seems to change the URL of the page?Davidadavidde
not sure why, but i'm not getting the same binary as going directly to the URL ( i'm testing on an excel file ) direct URL in the browser -> open w/o a warning in Excel, and via fetch - raise a warning that it might be corrupted (?) ( clicking "Yes" to load it anyway - is loading it OK with all the data ... )Missis
A
66

I temporarily solve this problem by using download.js and blob.

let download = require('./download.min');

...

function downloadFile(token, fileId) {
  let url = `https://www.googleapis.com/drive/v2/files/${fileId}?alt=media`;
  return fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': token
    }
  }).then(function(resp) {
    return resp.blob();
  }).then(function(blob) {
    download(blob);
  });
}

It's working for small files, but maybe not working for large files. I think I should dig Stream more.

Aureaaureate answered 13/9, 2015 at 2:54 Comment(6)
just bear in mind the current blob size limit is around 500mb for browsersGrandstand
Nice, just one thing: would it be possible to get the file name from the server response so to let the user download it with its real name?Escolar
@Escolar to do so, you should pass an object from server, which contains not only the data but also the name. Once you do that, you can deal with its field separately, but for that you should see other answers here as (as far as I understand) .blob method is specific one of object which fetch resolves withDag
You need a node plugin that can resolve require -see first line - on the client side, such as bundler.js and make the download package available.Oreopithecus
Update on blob size limit: it's 2GB for desktop in 2022Largeminded
I get: "Failed to execute 'createObjectURL' on 'URL': Overload resolution failed."Blomquist
G
23

function download(dataurl, filename) {
  var a = document.createElement("a");
  a.href = dataurl;
  a.setAttribute("download", filename);
  a.click();
  return false;
}

download("data:text/html,HelloWorld!", "helloWorld.txt");

or:

function download(url, filename) {
fetch(url).then(function(t) {
    return t.blob().then((b)=>{
        var a = document.createElement("a");
        a.href = URL.createObjectURL(b);
        a.setAttribute("download", filename);
        a.click();
    }
    );
});
}

download("https://get.geojs.io/v1/ip/geo.json","geoip.json")
download("data:text/html,HelloWorld!", "helloWorld.txt");
Guilford answered 17/3, 2019 at 11:1 Comment(0)
B
9

Using dowloadjs. This will parse the filename from the header.

fetch("yourURL", {
    method: "POST",
    body: JSON.stringify(search),
    headers: {
        "Content-Type": "application/json; charset=utf-8"
    }
    })
    .then(response => {
        if (response.status === 200) {
            filename = response.headers.get("content-disposition");
            filename = filename.match(/(?<=")(?:\\.|[^"\\])*(?=")/)[0];
            return response.blob();
        } else {
        return;
        }
    })
    .then(body => {
        download(body, filename, "application/octet-stream");
    });
};
Beabeach answered 3/5, 2019 at 0:6 Comment(3)
This mostly worked, but I ended up using the regex from this other answer instead. So... fileName = fileName.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1] ? fileName.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)[1] : fileName;Threecolor
Firefox does not support filename.match(), I replaced that part with: filename = response.headers.get("content-disposition").split(";")[1].split('"')[1]; . Besides, the server must declare the header Access-Control-Expose-Headers: Content-Disposition in order to allow the browser to read the content-disposition header.Dunston
This mostly works. Except Apple is behind times and your regex fails on Safari. :( aizquier's answer works.Tubulate
T
7

Here is an example using node-fetch for anyone that finds this.

reportRunner({url, params = {}}) {
    let urlWithParams = `${url}?`
    Object.keys(params).forEach((key) => urlWithParams += `&${key}=${params[key]}`)
    return fetch(urlWithParams)
        .then(async res => ({
            filename: res.headers.get('content-disposition').split('filename=')[1],
            blob: await res.blob()
        }))
        .catch(this.handleError)
}
Thirdrate answered 15/7, 2018 at 10:33 Comment(3)
Where does this save it to? @Michael HobbsSurcharge
The function returns an object {filename, blob}, you can use fs.writefile to save the file to disk.Thirdrate
probably one of the rare examples that shows how to get download filename & typeConstitution
I
6

As per some of the other answers, you can definitely use window.fetch and download.js to download a file. However, using window.fetch with blob has the restriction on memory imposed by the browser, and the download.js also has its compatibility restrictions.

If you need to download a big-sized file, you don't want to put it in the memory of the client side to stress the browser, right? Instead, you probably prefer to download it via a stream. In such a case, using an HTML link to download a file is one of the best/simplest ways, especially for downloading big-sized files via a stream.

Step One: create and style a link element

You can make the link invisible but still actionable.

HTML:

<a href="#" class="download-link" download>Download</a>

CSS:

.download-link {
  position: absolute;
  top: -9999px;
  left: -9999px;
  opacity: 0;
}

Step Two: Set the href of the link, and trigger the click event

JavaScript

let url = `https://www.googleapis.com/drive/v2/files/${fileId}?alt=media`;

const downloadLink = document.querySelector('.download-link')
downloadLink.href = url + '&ts=' + new Date().getTime() // Prevent cache
downloadLink.click()

Notes:

  • You can dynamically generate the link element if necessary.
  • This approach is especially useful for downloading, via a stream, big-sized files that are dynamically generated on the server side
Immerse answered 26/9, 2019 at 11:15 Comment(4)
how could you make the CSS above Inline in the HTML?Louanne
@JeffR, it would like something like this: <a href="#" class="download-link" style="position: absolute; top: -9999px; left: -9999px; opacity: 0" download>Download</a>, and the class attribute here is only for querySelector to use in the JS code.Immerse
When I execute the line 'downloadLink.click()', the rest of my HTML code disappears. Any reason for that? If I comment the 'downloadLink.click()' out and instead show the Download link, all html works fine. Any ideas?Louanne
@Louanne Maybe something to do with the download attribute of the a HTML element. This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. This attribute only works for same-origin URLs. Also try without the '&ts=' + new Date().getTime() part, to see if it makes any difference to your case.Immerse
M
5

A similar but cleaner and more reliable solution IMO.

On your fetch function...

fetch(...)    
.then(res => 
    {
        //you may want to add some validation here
        downloadFile(res);
    }
)

and the downloadFile function is...

async function downloadFile(fetchResult) {        
    var filename = fetchResult.headers.get('content-disposition').split('filename=')[1];
    var data = await fetchResult.blob();
    // It is necessary to create a new blob object with mime-type explicitly set
    // otherwise only Chrome works like it should
    const blob = new Blob([data], { type: data.type || 'application/octet-stream' });
    if (typeof window.navigator.msSaveBlob !== 'undefined') {
        // IE doesn't allow using a blob object directly as link href.
        // Workaround for "HTML7007: One or more blob URLs were
        // revoked by closing the blob for which they were created.
        // These URLs will no longer resolve as the data backing
        // the URL has been freed."
        window.navigator.msSaveBlob(blob, filename);
        return;
    }
    // Other browsers
    // Create a link pointing to the ObjectURL containing the blob
    const blobURL = window.URL.createObjectURL(blob);
    const tempLink = document.createElement('a');
    tempLink.style.display = 'none';
    tempLink.href = blobURL;
    tempLink.setAttribute('download', filename);
    // Safari thinks _blank anchor are pop ups. We only want to set _blank
    // target if the browser does not support the HTML5 download attribute.
    // This allows you to download files in desktop safari if pop up blocking
    // is enabled.
    if (typeof tempLink.download === 'undefined') {
        tempLink.setAttribute('target', '_blank');
    }
    document.body.appendChild(tempLink);
    tempLink.click();
    document.body.removeChild(tempLink);
    setTimeout(() => {
        // For Firefox it is necessary to delay revoking the ObjectURL
        window.URL.revokeObjectURL(blobURL);
    }, 100);
}

(downloadFile function source: https://gist.github.com/davalapar/d0a5ba7cce4bc599f54800da22926da2)

Mephitis answered 1/7, 2021 at 19:21 Comment(1)
This is a helpful solution, thanks! As written the filename value comes through twice in filename. Changing the first line of downloadFile to var filename = fetchResult.headers.get('content-disposition').split('filename=')[1].split(';')[0].slice(1, -1); to strip off the second part (UTF/percent encoded) and the leading and trailing quotation marks works perfectly for me. (Assuming no semicolon in the filename!)Tubulate
B
4

No libraries only fetch API. Also you can change the file name

function myFetch(textParam, typeParam) {
  fetch("http://localhost:8000/api", {
    method: "POST",
    headers: {
      Accept: "application/json, text/plain, */*",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text: textParam,
      barcode_type_selection: typeParam,
    }),
  })
    .then((response) => {
      return response.blob();
    })
    .then((blob) => {
      downloadFile(blob);
    });
}

Here is the download file function

function downloadFile(blob, name = "file.pdf") {
  const href = URL.createObjectURL(blob);
  const a = Object.assign(document.createElement("a"), {
    href,
    style: "display:none",
    download: name,
  });
  document.body.appendChild(a);
  a.click();
  URL.revokeObjectURL(href);
  a.remove();
}
Boroughenglish answered 20/9, 2022 at 12:54 Comment(0)
D
2

I tried window.fetch but that ended up being complicated with my REACT app

now i just change window.location.href and add query params like the jsonwebtoken and other stuff.


///==== client side code =====
var url = new URL(`http://${process.env.REACT_APP_URL}/api/mix-sheets/list`);
url.searchParams.append("interval",data.interval);
url.searchParams.append("jwt",token)

window.location.href=url;

// ===== server side code =====

// on the server i set the content disposition to a file
var list = encodeToCsv(dataToEncode);
res.set({"Content-Disposition":`attachment; filename=\"FileName.csv\"`});
res.status(200).send(list)

the end results actually end up being pretty nice, the window makes request and downloads the file and doesn't event switch move the page away, its as if the window.location.href call was like a lowkey fetch() call.

Doris answered 26/8, 2019 at 14:51 Comment(0)
G
2

This is Lucas Matos answer (no libraries only fetch API) but with support for a custom name.

const url ='http://sample.example.file.doc'
const authHeader ="Bearer 6Q************" 

const options = {
  headers: {
    Authorization: authHeader
  }
};
 fetch(url, options)
  .then( res => res.blob() )
  .then( blob => {
    var fileURL = URL.createObjectURL(blob);
    var fileLink = document.createElement('a');
    fileLink.href = fileURL;
    fileLink.download = `whatever.ext`;
    fileLink.click();
  });

This solution does not allow you to change filename for the downloaded file. The filename will be a random uuid.

Gareth answered 25/8, 2023 at 23:14 Comment(0)
A
1

I guess the correct today answer is

fetch(window.location).then(async res=>res.body.pipeTo(await (await showSaveFilePicker({
  suggestedName: 'Any-suggestedName.txt'
})).createWritable()));
Alainaalaine answered 13/5, 2023 at 19:3 Comment(2)
I get "ReferenceError: showSaveFilePicker is not defined"Lycaon
showSaveFilePicker has very limited browser support stillScup

© 2022 - 2024 — McMap. All rights reserved.