Skip to content

Commit fda1467

Browse files
committed
Deprecated send_file etag support and mimetype guessing for file-like objects. This fixes pallets#104
1 parent faa1c71 commit fda1467

File tree

4 files changed

+116
-32
lines changed

4 files changed

+116
-32
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Release date to be announced, codename to be selected
1313
behaviour for `OPTIONS` responses.
1414
- Unbound locals now raise a proper :exc:`RuntimeError` instead
1515
of an :exc:`AttributeError`.
16+
- Mimetype guessing and etag support based on file objects is now
17+
deprecated for :func:`flask.send_file` because it was unreliable.
18+
Pass filenames instead or attach your own etags and provide a
19+
proper mimetype by hand.
1620

1721
Version 0.6.1
1822
-------------

docs/upgrading.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they
2727
are unbound. If you cought these exceptions with :exc:`AttributeError`
2828
before, you should catch them with :exc:`RuntimeError` now.
2929

30+
Additionally the :func:`~flask.send_file` function is now issuing
31+
deprecation warnings if you depend on functionality that will be removed
32+
in Flask 1.0. Previously it was possible to use etags and mimetypes
33+
when file objects were passed. This was unreliable and caused issues
34+
for a few setups. If you get a deprecation warning, make sure to
35+
update your application to work with either filenames there or disable
36+
etag attaching and attach them yourself.
37+
38+
Old code::
39+
40+
return send_file(my_file_object)
41+
return send_file(my_file_object)
42+
43+
New code::
44+
45+
return send_file(my_file_object, add_etags=False)
46+
3047
Version 0.6
3148
-----------
3249

flask/helpers.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
259259
260260
By default it will try to guess the mimetype for you, but you can
261261
also explicitly provide one. For extra security you probably want
262-
to sent certain files as attachment (HTML for instance).
262+
to sent certain files as attachment (HTML for instance). The mimetype
263+
guessing requires a `filename` or an `attachment_filename` to be
264+
provided.
263265
264266
Please never pass filenames to this function from user sources without
265267
checking them first. Something like this is usually sufficient to
@@ -274,6 +276,12 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
274276
The `add_etags`, `cache_timeout` and `conditional` parameters were
275277
added. The default behaviour is now to attach etags.
276278
279+
.. versionchanged:: 0.7
280+
mimetype guessing and etag support for file objects was
281+
deprecated because it was unreliable. Pass a filename if you are
282+
able to, otherwise attach an etag yourself. This functionality
283+
will be removed in Flask 1.0
284+
277285
:param filename_or_fp: the filename of the file to send. This is
278286
relative to the :attr:`~Flask.root_path` if a
279287
relative path is specified.
@@ -295,8 +303,25 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
295303
filename = filename_or_fp
296304
file = None
297305
else:
306+
from warnings import warn
298307
file = filename_or_fp
299308
filename = getattr(file, 'name', None)
309+
310+
# XXX: this behaviour is now deprecated because it was unreliable.
311+
# removed in Flask 1.0
312+
if not attachment_filename and not mimetype \
313+
and isinstance(filename, basestring):
314+
warn(DeprecationWarning('The filename support for file objects '
315+
'passed to send_file is not deprecated. Pass an '
316+
'attach_filename if you want mimetypes to be guessed.'),
317+
stacklevel=2)
318+
if add_etags:
319+
warn(DeprecationWarning('In future flask releases etags will no '
320+
'longer be generated for file objects passed to the send_file '
321+
'function because this behaviour was unreliable. Pass '
322+
'filenames instead if possible, otherwise attach an etag '
323+
'yourself based on another value'), stacklevel=2)
324+
300325
if filename is not None:
301326
if not os.path.isabs(filename):
302327
filename = os.path.join(current_app.root_path, filename)

tests/flask_tests.py

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,27 @@
3333
SECRET_KEY = 'devkey'
3434

3535

36+
@contextmanager
37+
def catch_warnings():
38+
"""Catch warnings in a with block in a list"""
39+
import warnings
40+
filters = warnings.filters
41+
warnings.filters = filters[:]
42+
old_showwarning = warnings.showwarning
43+
log = []
44+
def showwarning(message, category, filename, lineno, file=None, line=None):
45+
log.append(locals())
46+
try:
47+
warnings.showwarning = showwarning
48+
yield log
49+
finally:
50+
warnings.filters = filters
51+
warnings.showwarning = old_showwarning
52+
53+
3654
@contextmanager
3755
def catch_stderr():
56+
"""Catch stderr in a StringIO"""
3857
old_stderr = sys.stderr
3958
sys.stderr = rv = StringIO()
4059
try:
@@ -834,46 +853,64 @@ def test_send_file_xsendfile(self):
834853

835854
def test_send_file_object(self):
836855
app = flask.Flask(__name__)
837-
with app.test_request_context():
838-
f = open(os.path.join(app.root_path, 'static/index.html'))
839-
rv = flask.send_file(f)
840-
with app.open_resource('static/index.html') as f:
841-
assert rv.data == f.read()
842-
assert rv.mimetype == 'text/html'
856+
with catch_warnings() as captured:
857+
with app.test_request_context():
858+
f = open(os.path.join(app.root_path, 'static/index.html'))
859+
rv = flask.send_file(f)
860+
with app.open_resource('static/index.html') as f:
861+
assert rv.data == f.read()
862+
assert rv.mimetype == 'text/html'
863+
# mimetypes + etag
864+
assert len(captured) == 2
843865

844866
app.use_x_sendfile = True
845-
with app.test_request_context():
846-
f = open(os.path.join(app.root_path, 'static/index.html'))
847-
rv = flask.send_file(f)
848-
assert rv.mimetype == 'text/html'
849-
assert 'x-sendfile' in rv.headers
850-
assert rv.headers['x-sendfile'] == \
851-
os.path.join(app.root_path, 'static/index.html')
867+
with catch_warnings() as captured:
868+
with app.test_request_context():
869+
f = open(os.path.join(app.root_path, 'static/index.html'))
870+
rv = flask.send_file(f)
871+
assert rv.mimetype == 'text/html'
872+
assert 'x-sendfile' in rv.headers
873+
assert rv.headers['x-sendfile'] == \
874+
os.path.join(app.root_path, 'static/index.html')
875+
# mimetypes + etag
876+
assert len(captured) == 2
852877

853878
app.use_x_sendfile = False
854879
with app.test_request_context():
855-
f = StringIO('Test')
856-
rv = flask.send_file(f)
857-
assert rv.data == 'Test'
858-
assert rv.mimetype == 'application/octet-stream'
859-
f = StringIO('Test')
860-
rv = flask.send_file(f, mimetype='text/plain')
861-
assert rv.data == 'Test'
862-
assert rv.mimetype == 'text/plain'
880+
with catch_warnings() as captured:
881+
f = StringIO('Test')
882+
rv = flask.send_file(f)
883+
assert rv.data == 'Test'
884+
assert rv.mimetype == 'application/octet-stream'
885+
# etags
886+
assert len(captured) == 1
887+
with catch_warnings() as captured:
888+
f = StringIO('Test')
889+
rv = flask.send_file(f, mimetype='text/plain')
890+
assert rv.data == 'Test'
891+
assert rv.mimetype == 'text/plain'
892+
# etags
893+
assert len(captured) == 1
863894

864895
app.use_x_sendfile = True
865-
with app.test_request_context():
866-
f = StringIO('Test')
867-
rv = flask.send_file(f)
868-
assert 'x-sendfile' not in rv.headers
896+
with catch_warnings() as captured:
897+
with app.test_request_context():
898+
f = StringIO('Test')
899+
rv = flask.send_file(f)
900+
assert 'x-sendfile' not in rv.headers
901+
# etags
902+
assert len(captured) == 1
869903

870904
def test_attachment(self):
871905
app = flask.Flask(__name__)
872-
with app.test_request_context():
873-
f = open(os.path.join(app.root_path, 'static/index.html'))
874-
rv = flask.send_file(f, as_attachment=True)
875-
value, options = parse_options_header(rv.headers['Content-Disposition'])
876-
assert value == 'attachment'
906+
with catch_warnings() as captured:
907+
with app.test_request_context():
908+
f = open(os.path.join(app.root_path, 'static/index.html'))
909+
rv = flask.send_file(f, as_attachment=True)
910+
value, options = parse_options_header(rv.headers['Content-Disposition'])
911+
assert value == 'attachment'
912+
# mimetypes + etag
913+
assert len(captured) == 2
877914

878915
with app.test_request_context():
879916
assert options['filename'] == 'index.html'
@@ -884,7 +921,8 @@ def test_attachment(self):
884921

885922
with app.test_request_context():
886923
rv = flask.send_file(StringIO('Test'), as_attachment=True,
887-
attachment_filename='index.txt')
924+
attachment_filename='index.txt',
925+
add_etags=False)
888926
assert rv.mimetype == 'text/plain'
889927
value, options = parse_options_header(rv.headers['Content-Disposition'])
890928
assert value == 'attachment'

0 commit comments

Comments
 (0)