Skip to content

MongoEngine api_file_view omits Content-Disposition — downloads have no filename or extension #2916

Description

@hasansezertasan

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:

  1. Define a MongoEngine Document with a FileField.
  2. Register a ModelView for it.
  3. Upload any file (e.g. report.pdf) through the admin.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions