Download Files Using Fetch
Recently I was developing a pretty straightforward React component that fetches data and renders it as an HTML table, but also allowed the user to download the data in either PDF or CSV format. On the surface, this looked like something I'd done many, many times. Easy.
Not so fast though. The backend API accepts a set of search filters, which are
sent as the body of a POST request, in JSON format. That's no problem for the
fetch request that I use to populate the table, but what about the download
buttons? Typically, to provide filters when downloading a file you have to
either 1) use a GET request with query parameters, or 2) construct a hidden form
and submit it programmatically to POST some data to the backend. Neither of
these methods would work in this case, however, because the API doesn't support
GET and a POST-ed form would submit the data with a content-type of
application/x-www-form-urlencoded (we need application/json). I was about to
reach out to the backend developer to ask for an API change, but I took a few
seconds to ask google, and I found something unexpected.
You can download things with the fetch API now! It's a little tricky and there are some caveats but it works beautifully. Awesome!
OK, enough talk, let's dig in to see which APIs make this possible.
Of course, you need to fetch something. You're probably already familiar with this API but I think it makes sense to start with the fetch code so you can see what we're sending and what we're receiving. Let's say, for example, you want to fetch a list of products matching some user-specified filters, and download the results as a PDF file. You'll write something like the following function, which performs a pretty standard POST request, using the fetch API.
OK, now that we've got a response back from the server, we need to do something
with it. The first step is to read the response, and that's where
response.blob() comes in.
You've probably used
response.text(). These APIs load
the response body and, in the case of
.json(), parse it too. In this case,
however, we're dealing with binary data; it's not text-based. So instead of
those APIs, we'll use
response.blob(), which loads the response body as a Blob
This one threw me for a loop. Apparently you can stash a Blob or File object
somewhere in the browser's memory and create a URL that references the stored
object. When you call it, you get a URL back that references the object. The URL
includes the blob protocol, the current site's protocol & hostname, and a unique
identifier for the file. It might look something like this:
This is the DOM API for hyperlink elements. We're going to create an instance
document.createElement, and that hyperlink instance is going to
reference the object URL we created earlier. After we've created and configured
the link, we can programmatically click it without even adding it to the DOM!
At this point, the browser downloads the file. Amazing!
We're not done yet though. That file is still stored in memory, so if the user
downloads lots of files (or big files), that could cause problems. Thankfully,
there's a way to clean that all up. After the call to
link.click(), we can
clear the file from memory using
It's simple to use, but apparently a lot of people forget that step.
Putting it all together
response.blob() reads the entire response body (the file) into memory
Ideally, when dealing with large files you'll want to use streams to avoid running out of memory. Unfortunately, these APIs don't work with streams, so be careful and avoid using this to download large files.
Don't forget to revoke the object URL!
Again, this stores the entire file in memory. You need to manually remove it from memory when it's no longer needed to free up space. Otherwise, your web app will consume more and more of the user's memory when they download multiple files.
Chrome might block downloads
Chrome added a new security feature late last year (2020) that prevents web sites from spamming users with file download requests. This is good, but it might cause problems as your users click on buttons that programmatically initiate file downloads. When chrome blocks the download, it has an indicator in the location bar, but it's not always obvious if you don't know what to look for.