Summary
ModelView.api_file_view() in flask_admin.contrib.mongoengine.view serves
GridFS file content with Content-Type and Content-Length headers but no
Content-Disposition. Browsers therefore fall back to the URL's last path
segment (/admin/<view>/api/file/) when saving, producing a file named
file (or file.html after a content-sniff) with no extension — even though
GridFS knows the original filename.
This was originally reported in #2036 (Sep 2020). The PR there bundled an
unrelated DBRef fix, went stale, and now conflicts with master. Opening a
focused issue so the fix can be discussed and landed on its own.
Expected behavior
Clicking a FileField download link in the admin should save the file with
its original name and extension as stored in GridFS (data.filename).
Actual behavior
The response carries no Content-Disposition header, so the browser names
the download from the URL path:
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 24576
→ saved as file (no extension), regardless of what was uploaded.
Root cause
flask_admin/contrib/mongoengine/view.py:727:
return Response(
data.read(),
content_type=data.content_type,
headers={"Content-Length": data.length},
)
data is a gridfs.GridOut and exposes .filename, which is what should
drive Content-Disposition.
Notes on PR #2036
The original PR proposed:
headers={
"Content-Length": data.length,
"Content-disposition": "attachment; filename=%(filename)s" % {"filename": data.filename},
}
This works for ASCII filenames without spaces but breaks for:
- filenames containing spaces, commas, semicolons, or quotes (header parsing
ambiguity);
- non-ASCII filenames (RFC 6266 requires
filename*=UTF-8''... encoding);
- filenames containing CR/LF (header injection —
data.filename ultimately
comes from user upload).
A correct fix should either use werkzeug.utils.send_file (which handles
RFC 6266 encoding and range requests for free) or call
werkzeug.datastructures.Headers.set with the filename keyword, which
delegates to the same encoder.
Possible fix sketch (for discussion, not part of this issue)
from werkzeug.utils import send_file
return send_file(
data, # GridOut is a file-like object
mimetype=data.content_type,
download_name=data.filename,
as_attachment=True,
)
send_file will set Content-Length, Content-Type, and an RFC-6266-safe
Content-Disposition for us, and supports Range requests — which the
current handcrafted Response does not.
Reproducer
A full MRE requires a running MongoDB instance, so it's not as self-contained
as the PEP 723 scripts used elsewhere. The minimal path:
- Define a MongoEngine
Document with a FileField.
- Register a
ModelView for it.
- Upload any file (e.g.
report.pdf) through the admin.
- Click the download link on the details view → file is saved as
file
instead of report.pdf.
The response headers can be inspected directly with curl -I against the
/admin/<view>/api/file/?id=...&coll=... URL to confirm
Content-Disposition is absent.
Environment
flask-admin from master (commit at time of writing).
mongoengine (any recent version).
- Any modern browser, or
curl.
Related
Disclosure: I drafted this issue with help from Claude Code while triaging
stale PR #2036; the references above were verified manually.
Summary
ModelView.api_file_view()inflask_admin.contrib.mongoengine.viewservesGridFS file content with
Content-TypeandContent-Lengthheaders but noContent-Disposition. Browsers therefore fall back to the URL's last pathsegment (
/admin/<view>/api/file/) when saving, producing a file namedfile(orfile.htmlafter a content-sniff) with no extension — even thoughGridFS knows the original filename.
This was originally reported in #2036 (Sep 2020). The PR there bundled an
unrelated
DBReffix, went stale, and now conflicts withmaster. Opening afocused issue so the fix can be discussed and landed on its own.
Expected behavior
Clicking a
FileFielddownload link in the admin should save the file withits original name and extension as stored in GridFS (
data.filename).Actual behavior
The response carries no
Content-Dispositionheader, so the browser namesthe download from the URL path:
→ saved as
file(no extension), regardless of what was uploaded.Root cause
flask_admin/contrib/mongoengine/view.py:727:datais agridfs.GridOutand exposes.filename, which is what shoulddrive
Content-Disposition.Notes on PR #2036
The original PR proposed:
This works for ASCII filenames without spaces but breaks for:
ambiguity);
filename*=UTF-8''...encoding);data.filenameultimatelycomes from user upload).
A correct fix should either use
werkzeug.utils.send_file(which handlesRFC 6266 encoding and range requests for free) or call
werkzeug.datastructures.Headers.setwith thefilenamekeyword, whichdelegates to the same encoder.
Possible fix sketch (for discussion, not part of this issue)
send_filewill setContent-Length,Content-Type, and an RFC-6266-safeContent-Dispositionfor us, and supportsRangerequests — which thecurrent handcrafted
Responsedoes not.Reproducer
A full MRE requires a running MongoDB instance, so it's not as self-contained
as the PEP 723 scripts used elsewhere. The minimal path:
Documentwith aFileField.ModelViewfor it.report.pdf) through the admin.fileinstead of
report.pdf.The response headers can be inspected directly with
curl -Iagainst the/admin/<view>/api/file/?id=...&coll=...URL to confirmContent-Dispositionis absent.Environment
flask-adminfrommaster(commit at time of writing).mongoengine(any recent version).curl.Related
focused PRs).