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)
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With