Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set Unicode filename in Flask response header

I am trying to set the Content-Disposition header to send a file to the client. The file name is Unicode. When I try to set the header, it fails with a UnicodeEncodeError. I tried various combinations of encode and decode but couldn't get it to work. How can I send a file with a Unicode filename?

destination_file = 'python_report.html'
response.headers['Content-Disposition'] = 'attachment; filename=' + destination_file
  File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/http/server.py", line 495, in send_header
    ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 41-42: ordinal not in range(256)
like image 982
Murtuza Z Avatar asked Apr 12 '17 09:04

Murtuza Z


1 Answers

RFC 2231 section 4 describes how to specify an encoding to use instead of Latin-1 for a header value. Use the header option filename*=UTF-8''..., where ... is the url-encoded name. You can also include the filename option to provide a Latin-1 fallback.

Until relatively recently, browsers did not consistently support this. This page has some metrics on browser support. Notably, IE8 will ignore the UTF-8 option, and will fail completely if the UTF-8 option comes before the Latin-1 option.

Flask 1.0 supports calling send_file with Unicode filenames. If you use Flask 1.0, you can use send_file with as_attachment=True and a Unicode filename.

from flask import send_file

@app.route('/send-python-report')
def send_python_report():
    return send_file('python_report.html', as_attachment=True)

Until then, you can construct the header manually using the same process Flask will use.

import unicodedata

from flask import send_file
from werkzeug.urls import url_quote

@app.route('/send-python-report')
def send_python_report():
    filename = 'python_report.html'
    rv = send_file(filename)

    try:
        filename = filename.encode('latin-1')
    except UnicodeEncodeError:
        filenames = {
            'filename': unicodedata.normalize('NFKD', filename).encode('latin-1', 'ignore'),
            'filename*': "UTF-8''{}".format(url_quote(filename)),
        }
    else:
        filenames = {'filename': filename}

    rv.headers.set('Content-Disposition', 'attachment', **filenames)
    return rv

For security you should use send_from_directory instead if the filename is provided by user input. The process is the same as above, substituting the function.


WSGI doesn't ensure the order of header options, so if you want to support IE8 you must construct the header value completely manually using dump_options_header with an OrderedDict. Otherwise, filename* may appear before filename, which as noted above does not work in IE8.

from collections import OrderedDict
import unicodedata

from flask import send_file
from werkzeug.http import dump_options_header
from werkzeug.urls import url_quote

@app.route('/send-python-report')
def send_python_report():
    filename = 'python_report.html'
    rv = send_file(filename)
    filenames = OrderedDict()

    try:
        filename = filename.encode('latin-1')
    except UnicodeEncodeError:
        filenames['filename'] = unicodedata.normalize('NFKD', filename).encode('latin-1', 'ignore')
        filenames['filename*']: "UTF-8''{}".format(url_quote(filename))
    else:
        filenames['filename'] = filename

    rv.headers.set('Content-Disposition', dump_options_header('attachment', filenames))
    return rv
like image 76
davidism Avatar answered Nov 14 '22 09:11

davidism