Downloading and saving data with fetch() from authenticated REST

In reference to this answer, you can use FileSaver or download.js libraries.

Example:

var saveAs = require('file-saver');

fetch('/download/urf/file', {
  headers: {
    'Content-Type': 'text/csv'
  },
  responseType: 'blob'
}).then(response => response.blob())
  .then(blob => saveAs(blob, 'test.csv'));

I was forced to come back to this because we just ran into the 2MB limit in Chrome. Who knew? (This person, for one: https://craignicol.wordpress.com/2016/07/19/excellent-export-and-the-chrome-url-limit/ posted a couple months after my question here, and whose solution I implemented below.)

Anyhow, I will now attempt to answer my own questions since I have not seen an acceptable answer yet regarding the requested authentication requirement. (Although, Conrado did provide some useful links otherwise to that requirement.)

As to the questions of: Why I have to do it this way and is there not a more intrinsic way? The answers appear to be "just because" and "no" respectively. Not very good answers, I know. I wish I could explain it more... but I really can't without sticking my foot in my mouth somewhere. I'm not a network expert and certainly not the right person to explain it. It just is what it is from what I read. You just can't fake a stream as a file download while authenticating.

As to: What I am missing and what is the easier way? Well, there is no easier way. Sure, I could throw more libraries like FileSaver.js at the code base and let them hide some of this code for me. But, I don't like a larger tool set than I really need (I'm looking at you, you ridiculous 50MB sites). I could accomplish the same thing as those libraries by hiding my download() function elsewhere and importing it. No, that is not easier from my point of view. Less work maybe to grab a prebuilt function, but not easier in terms of amount of code executed to make a download happen. Sorry.

But... I was missing something: that thing that led to that 2MB limit in Chrome. At the time, I didn't really understand how this URI data hack was working that I was using. I found some code that worked, and used it. I get it now -- now that I've had more time to read deeper on that part of the problem. In short, I was missing the blob options versus the URI option. Sure, blobs have their own limitations with various browsers but, given that my use cases would not have been affected by any of those back in 2016, the blob option would have been a better route to take from the start... and it feels less hacky (and maybe a bit "easier" because of that alone).

Here is my current solution that tries a blob save before falling back to the URI data hack:

JS (React):

saveStreamCSV(filename, text) {
    this.setState({downloadingCSV: false})
    if(window.navigator.msSaveBlob) {
        // IE 10 and later, and Edge.
        var blobObject = new Blob([text], {type: 'text/csv'});
        window.navigator.msSaveBlob(blobObject, filename);
    } else {
        // Everthing else (except old IE).
        // Create a dummy anchor (with a download attribute) to click.
        var anchor = document.createElement('a');
        anchor.download = filename;
        if(window.URL.createObjectURL) {
            // Everything else new.
            var blobObject = new Blob([text], {type: 'text/csv'});
            anchor.href = window.URL.createObjectURL(blobObject);
        } else {
            // Fallback for older browsers (limited to 2MB on post-2010 Chrome).
            // Load up the data into the URI for "download."
            anchor.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(text);
        }
        // Now, click it.
        if (document.createEvent) {
            var event = document.createEvent('MouseEvents');
            event.initEvent('click', true, true);
            anchor.dispatchEvent(event);
        }
        else {
            anchor.click();
        }
    }
}
handleDownloadClick(e) {
    this.setState({downloadingCSV: true})
    fetch(`/api/admin/studies/${this.props.study.id}/csv`
    ,   {
            headers: {
                "Authorization": "Bearer " + this.props.authAPI.getToken()
            ,   "Accept": "text/csv"
            }
        }
    )
    .then((response) => response.text())
    .then((responseText) => this.saveStreamCSV(`study${this.props.study.id}.csv`, responseText))
    .catch((error) => {
        this.setState({downloadingCSV: false})
        console.error("CSV handleDownloadClick:", error)
    })
}

Note: I went this route only because I don't need to worry about all the use cases that FileSaver.js was built for (this is for a feature on the admin app only and not public-facing).


Tell the browser how to handle your download by using the DOM. Inject the anchor tag with Node.appendChild and let the user click the link.

<a href="/api/admin/studies/1/csv" download="study1.csv">Download CSV</a>

What you are doing is downloading the file, inserting the already complete request into an anchor tag, and creating a second request by using pom.click() in your code.

Edit: I missed the authorization header. If you like this suggestion, you could put the token in the query string instead.