11

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

David
  • 923
  • 1
  • 9
  • 11
  • The context manager ensures the file gets closed before the function ends, you can't return the file handle that way. Why not pass the file name instead? – jonrsharpe May 01 '16 at 12:17
  • Thanks for letting me know about that! I'll try that now. – David May 01 '16 at 12:19
  • @jonrsharpe Nice, it works! I didn't know you could pass the file name string instead of an actual file. Thank you very much! – David May 01 '16 at 12:21

2 Answers2

5

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')
iurisilvio
  • 4,868
  • 1
  • 30
  • 36
4

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)

Newbie
  • 4,462
  • 11
  • 23
  • I think you downvoted me. Your examples where my code doesn't work are broken by spec. You should not close the stream, WSGI will do that after it consumed the stream. I answered with the correct way to use `send_file`. – iurisilvio Dec 04 '20 at 17:02
  • 1
    Yes I downvoted your answer, nothing personal. It is just due to your answer being simplistic in relation to the specific exception and send_file. As you know send file does support a file name or a file-like-object as input. And in the second case this exception is very common. I would like you to elaborate why you sustain my answer is wrong? – Newbie Dec 04 '20 at 18:37