最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Downloading and saving data with fetch() from authenticated REST - Stack Overflow

programmeradmin3浏览0评论

I have a React app working with a REST backend built in Python and Flask. I'm downloading data from a database and saving it as a CSV file through the browser. I have this working. What I don't understand, however, is why I had to go beyond the sources I've been reading and mash stuff up to get this to work. Why haven't I found this outlined better?

Some say all I have to do is set the response header with mimetype and Content-Disposition: attachment; filename=something.csv:

  • In python using Flask, how can I write out an object for download?
  • /
  • .html

Yet, this, alone, was only working with plain links, and not with fetch() and authentication, so I had to go looking for ways to save client data to disk such as this:

  • Using HTML5/Javascript to generate and save a file

So, my question is either:

  • Why do I have to do it this way?, or
  • What am I missing -- what is the easier way?

It appears that the answer to the first question is that I can't modify request headers (to add the auth token) except through XHR type work. This appears to be answered (non-answered, really) here:

  • Adding custom HTTP headers using JavaScript

And, that for some reason, responses to XHR with Content-Disposition: attachment are meaningless. Is that true? Is there not a more intrinsic way to manage requests like this in modern browsers?

I feel like I don't understand this enough and that bugs me.

Anyhow, here is the working code I am looking to simplify, if possible:

JS (React):

// 
download(filename, text) {
    var pom = document.createElement('a');
    pom.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(text));
    pom.setAttribute('download', filename);

    if (document.createEvent) {
        var event = document.createEvent('MouseEvents');
        event.initEvent('click', true, true);
        pom.dispatchEvent(event);
    }
    else {
        pom.click();
    }
}
downloadReport(studyID) {
    fetch(`/api/admin/studies/${studyID}/csv`
    ,   {
            headers: {
                "Authorization": "Bearer " + this.props.authAPI.getToken()
            ,   "Accept": "text/csv"
            }
        }
    )
    .then(this.checkStatus.bind(this))
    .then((response) => response.text())
    .then((responseText) => this.download(`study${studyID}.csv`, responseText))
    .catch((error) => {
        console.error(this.props.location.pathname, error)
    })
}

Python (Flask):

@app.route("/api/admin/studies/<int:study_id>/csv", methods=["GET"])
@admin.login_required
def admin_get_csv(study_id):

    test = [("1","2","3"),("4","5","6")]
    def generate():
        for row in test:
            yield ",".join(row) + "\n"

    return Response(
                generate()
            ,   mimetype="text/csv"
            ,   headers={"Content-Disposition": "attachment; filename=study{0}.csv".format(study_id)}
            )

I have a React app working with a REST backend built in Python and Flask. I'm downloading data from a database and saving it as a CSV file through the browser. I have this working. What I don't understand, however, is why I had to go beyond the sources I've been reading and mash stuff up to get this to work. Why haven't I found this outlined better?

Some say all I have to do is set the response header with mimetype and Content-Disposition: attachment; filename=something.csv:

  • In python using Flask, how can I write out an object for download?
  • http://code.stephenmorley.org/php/creating-downloadable-csv-files/
  • http://www.jtricks.com/bits/content_disposition.html

Yet, this, alone, was only working with plain links, and not with fetch() and authentication, so I had to go looking for ways to save client data to disk such as this:

  • Using HTML5/Javascript to generate and save a file

So, my question is either:

  • Why do I have to do it this way?, or
  • What am I missing -- what is the easier way?

It appears that the answer to the first question is that I can't modify request headers (to add the auth token) except through XHR type work. This appears to be answered (non-answered, really) here:

  • Adding custom HTTP headers using JavaScript

And, that for some reason, responses to XHR with Content-Disposition: attachment are meaningless. Is that true? Is there not a more intrinsic way to manage requests like this in modern browsers?

I feel like I don't understand this enough and that bugs me.

Anyhow, here is the working code I am looking to simplify, if possible:

JS (React):

// https://stackoverflow.com/a/18197511/680464
download(filename, text) {
    var pom = document.createElement('a');
    pom.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(text));
    pom.setAttribute('download', filename);

    if (document.createEvent) {
        var event = document.createEvent('MouseEvents');
        event.initEvent('click', true, true);
        pom.dispatchEvent(event);
    }
    else {
        pom.click();
    }
}
downloadReport(studyID) {
    fetch(`/api/admin/studies/${studyID}/csv`
    ,   {
            headers: {
                "Authorization": "Bearer " + this.props.authAPI.getToken()
            ,   "Accept": "text/csv"
            }
        }
    )
    .then(this.checkStatus.bind(this))
    .then((response) => response.text())
    .then((responseText) => this.download(`study${studyID}.csv`, responseText))
    .catch((error) => {
        console.error(this.props.location.pathname, error)
    })
}

Python (Flask):

@app.route("/api/admin/studies/<int:study_id>/csv", methods=["GET"])
@admin.login_required
def admin_get_csv(study_id):

    test = [("1","2","3"),("4","5","6")]
    def generate():
        for row in test:
            yield ",".join(row) + "\n"

    return Response(
                generate()
            ,   mimetype="text/csv"
            ,   headers={"Content-Disposition": "attachment; filename=study{0}.csv".format(study_id)}
            )
Share Improve this question edited Sep 26, 2019 at 21:44 juanitogan asked May 8, 2016 at 1:10 juanitoganjuanitogan 1,8581 gold badge23 silver badges37 bronze badges 1
  • Future readers might find this answer helpful as well. – Chris Commented Dec 27, 2023 at 10:02
Add a comment  | 

3 Answers 3

Reset to default 7

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).

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'));

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.

发布评论

评论列表(0)

  1. 暂无评论