6

I am developing a web application using HTML + plain Javascript in the frontend, and Flask as backend. The application sends some ID to the server, the server should generate a report as PDF file and send it back to the client.

I am using Flask for the backend and I have created the following endpoint:

    @app.route("/download_pdf", methods=['POST'])
    def download_pdf():
        if request.method == 'POST':
            report_id = request.form['reportid']
            print(report_id) //Testing purposes.
            // do some stuff with report_id and generate a pdf file.
            return send_file('/home/user/report.pdf', mimetype='application/pdf', as_attachment=True)
// I already tried different values for mimetype and as_attachment=False

From the command line I can test the endpoint and I get the right file, and the server console prints the 123 report_id as expected:

curl --form reportid=123 http://localhost:5000/download_pdf >> output.pdf

For the frontend side I created a button that calls a Javascript function:

<button id=pdf_button onclick="generatePdf()"> PDF </button>

The Javascript function looks like this:

function generatePdf(){
    var report_list = document.getElementById("report_list")

    if (report_list.selectedIndex < 0){
        alert("Please, select a report.");
    }else{
        var req = new XMLHttpRequest();

        req.open("POST", "/download_pdf", true);
        req.responseType = "document";
        req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        req.onreadystatechange = function(){
            console.log(req.readyState)
            console.log(req.status)
            console.log(req.response)
            var link = document.createElement('a')
            link.href = req.response;
            link.download="report.pdf"
            link.click()

        }
        var selected_value = report_list.options[report_list.selectedIndex].value;
        var params="reportid="+selected_value;
        req.send(params);
    }

};

req.response is null in this case. However, the call to the endpoint has been done correctly, as the backend console prints the report_id as expected.

Already tried:

Lastly, the Firefox console shows these 6 messages after pressing the related button (please, observe the console.log() calls in the previous code):

2
0
null
4
0
null

It seems that the Javascript function has been called twice when the button is pressed.

My goal is to get the PDF downloaded. I don't know if what am I doing wrong; I'd thank any help on this.

davidism
  • 121,510
  • 29
  • 395
  • 339
  • You are getting a response as null. responseType should be blob. And your request header is also wrong – weegee Jun 11 '19 at 15:36
  • If you are seeing the HTTP status code as 0, that usually comes with an error regarding CORS. If you're running this code from a local file (e.g., the address shows `file:///` or `C:\` or some other local filesystem construct), that's likely the cause. – Heretic Monkey Jun 11 '19 at 15:48
  • Doesn’t look like it. The user says that they have a backend so its sure that they are using the desired extension. You can also see the OP is on localhost @HereticMonkey – weegee Jun 11 '19 at 18:37
  • @weegee The curl statement is an isolated test they did against their back end, not how they are calling their front end. That's the only place they reference `localhost` in the question. Either way, status code 0 is most commonly associated with CORS problems. It would also align with the response being null. Whether it's because they're running from the filesystem or because they are trying to do XHR against a different origin is somewhat immaterial. – Heretic Monkey Jun 11 '19 at 19:45
  • Thanks for your comments. The solution provided by @weegee did not solve the problem - in fact, at some point, I had a similar code (I tried sooo many different things). I call the frontend from the browser using `http://localhost:5000/reports`, but typing the IP address of the server instead of `localhost` produces the same results. I'll research a bit more on the possible problems related with CORS and be back if I find a solution. – Alejandro Villegas Jun 12 '19 at 06:35
  • I changed the async call to the endpoint to a synchrounous call and it downloads the file (a few changes more are required). However, the downloaded file is blank but the sizes (both file size and pdf page size) are correct. It seems that now is an encoding problem which allows to read the PDF metadata correctly but corrupts the PDF binary data. I can't set the responseType property, as setting it is not compatible with synchronous calls. I'll post a solution once I fix it. – Alejandro Villegas Jun 12 '19 at 09:20
  • Well you shouldn’t really be using synchronous calls as they block the main UI. Why are you using these types of calls – weegee Jun 12 '19 at 13:28
  • I know I shouldn't use synchronous calls. I intend to make it work with a synchronous call to make sure the problem does not come from the asynchronous method, and then move on and implement an async version. – Alejandro Villegas Jun 12 '19 at 13:39

2 Answers2

3

Finally, I found what the problem was and I post this for the record. I thought it was unrelated, but the <button> calling the Javascript function was inside a <form>. I checked that the form was updated before the call to the endpoint finished, causing the call to finish prepaturely.

If somebody else needs this as example, a snipet of the final code is as follows:

HTML (both the select and button are not part of a <form>):

<select id="report_list" size=20> ... </select> ... <button id="pdf_button" onclick="generatePdf()"> PDF </button>

Javascript:

function generatePdf(){
    var report_list = document.getElementById("report_list");
    var req = XMLHttpRequest();
    var selected_value = report_list.options[report_list.selectedIndex].value;

    req.open("POST", "/reports/"+selected_value+"/pdf", true);
    req.responseType = "blob";
    req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    req.onreadystatechange = function(){
    if (this.readyState == 4 && this.status == 200){
        var blob = new Blob([this.response], {type: "application/pdf"});
        var url = window.URL.createObjectURL(blob);
        var link = document.createElement('a');
        document.body.appendChild(link);
        link.style = "display: none";
        link.href = url;
        link.download = "report.pdf";
        link.click();

        setTimeout(() => {
        window.URL.revokeObjectURL(url);
        link.remove(); } , 100);
    }
    };

    req.send();
}

Flask:

@app.route("/reports/<id>/pdf", methods=['POST'])
def get_pdf(id):
    if request.method == 'POST':
        return send_file(get_pdf_path(id), mimetype='application/pdf')

I am not sure if this is the best or more elegant way to get this done, but so far it works for me.

  • Worked for me, but only when I changed send_file to send_from_directory in flask:https://stackoverflow.com/questions/22916850/why-cant-flask-find-this-file – Jonathan Apr 18 '22 at 18:10
0

Your ajax settings are wrong, they should be like these

req.open("POST", "/download_pdf", true);
req.responseType = "blob";

req.onreadystatechange = function() {
  console.log(req.readyState)
  console.log(req.status)
  const blob = new Blob([req.response]);
  const url = window.URL.createObjectURL(blob);

  const link = document.createElement('a')
  link.href = url
  link.download = "report.pdf"
  link.click()
}

The response type should be blob and when you get the response, parse it as a blob. After some time, remove the link

setTimeout(() => {
    window.URL.revokeObjectURL(url);
    link.remove();
}, 100);
weegee
  • 3,256
  • 2
  • 18
  • 32