David
David

Reputation: 973

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 !!

Upvotes: 11

Views: 4726

Answers (3)

SomeGenericDev
SomeGenericDev

Reputation: 651

For me the problem was that I forgot to call read() on the IO object

Basically I had to turn

with open(myFilePath, "r") as myFile:
        return myFile

into

with open(myFilePath, "r") as myFile:
        return myFile.read()

Upvotes: 0

Newbie
Newbie

Reputation: 4829

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)

Upvotes: 6

iurisilvio
iurisilvio

Reputation: 4987

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

Upvotes: 5

Related Questions