Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python/Flask - ValueError: I/O operation on closed file

Before anyone says that this is a duplicate, I do not think it is because I have looked at the similar questions and they have not helped me!

I am creating a Flask server in python, and I need to be able to have a url that shows a pdf.

I tried to use the following code:

@app.route('/pdf')
def pdfStuff():

        with open('pdffile.pdf', 'rb') as static_file:
                return send_file(static_file, attachment_filename='pdffile.pdf')

This is supposed to make it so when I go to /pdf it will show the pdf file pdffile.pdf.

However, this does not work because when I run the code I get this error:

ValueError: I/O operation on closed file

How is this the case? My return statement is inside the with statement, therefore shouldn't the file be open?

I tried to use a normal static_file = open(...) and used try and finally statements, like this:

static_file = open('pdffile.pdf','rb')
try:
        return send_file(static_file, attachment_filename='pdffile.pdf')
finally:
        static_file.close()

The same error happens with the above code, and I have no idea why. Does anyone know what I could be doing wrong?

Sorry if I am being stupid and there is something simple that I made a mistake with!

Thank you very much in advance !!

like image 396
David Avatar asked May 01 '16 12:05

David


2 Answers

Despite @iurisilvio's answer solves this specific problem, is not a useful answer in any other case. I was struggling with this myself.

All the following examples are throwing ValueError: I/O operation on closed file. but why?

@app.route('/pdf')
def pdfStuff():
    with open('pdffile.pdf', 'rb') as static_file:
        return send_file(static_file, attachment_filename='pdffile.pdf')


@app.route('/pdf')
def pdfStuff():
    static_file = open('pdffile.pdf','rb')
    try:
        return send_file(static_file, attachment_filename='pdffile.pdf')
    finally:
        static_file.close()

I am doing something slightly different. Like this:

@page.route('/file', methods=['GET'])
def build_csv():

    # ... some query ...

    ENCODING = 'utf-8'
    bi = io.BytesIO()
    tw = io.TextIOWrapper(bi, encoding=ENCODING)
    c = csv.writer(tw)
    c.writerow(['col_1', 'col_2'])
    c.writerow(['1', '2'])

    bi.seek(0)
    return send_file(bi,
                     as_attachment=True,
                     attachment_filename='file.csv',
                     mimetype="Content-Type: text/html; charset={0}".format(ENCODING)
                     )

In the first two cases, the answer is simple:

You give a stream to send_file, this function will not immediatelly transmit the file, but rather wrap the stream and return it to Flask for future handling. Your pdfStuff function will allready return before Flask will start handling your stream, and in both cases (with and finally) the stream will be closed before your function returns.

The third case is more tricky (but this answer pointed me in the right direction: Why is TextIOWrapper closing the given BytesIO stream?). In the same fashion as explained above, bi is handled only after build_csv returns. Hence tw has allready been abandoned to the garbage collector. When the collector will destroy it, tw will implicitly close bi. The solution to this one is tw.detach() before returning (this will stop TextIOWrapper from affecting the stream).

Side note (please correct me if I'm wrong): This behaviour is limiting, unless when send_file is provided with a file-like object it will handle the closing on its own. It is not clear from the documentation (https://flask.palletsprojects.com/en/0.12.x/api/#flask.send_file) if closing is handled. I would assume so (there are some .close() present in the source code + send_file uses werkzeug.wsgi.FileWrapper which has .close() implemented too), in which case your approach can be corrected to:

@app.route('/pdf')
def pdfStuff():
    return send_file(open('pdffile.pdf','rb'), attachment_filename='pdffile.pdf')

Ofcourse in this case, would be stright forward to provide the file name. But in other cases, may be needed to wrap the file stream in some manipulation pipeline (decode / zip)

like image 101
Newbie Avatar answered Oct 05 '22 01:10

Newbie


Use send_file with the filename, it'll open, serve and close it the way you expect.

@app.route('/pdf')
def pdfStuff():
    return send_file('pdffile.pdf')
like image 40
iurisilvio Avatar answered Oct 05 '22 02:10

iurisilvio