From c14e9d5d20525e2014dfcee8050dd3dcc6a281d5 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 9 Jan 2026 18:54:35 +0530 Subject: [PATCH 1/2] fix(response): set content-disposition header correctly again Broke in ee2c4c20ce82b56095acfbc91169fbf73b054b2b Signed-off-by: Akhil Narang --- frappe/utils/response.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index b180b2cf3f..520d2a2837 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -298,10 +298,16 @@ def download_private_file(path: str) -> Response: return send_private_file(path.split("/private", 1)[1]) +FORCE_DOWNLOAD_EXTENSIONS = (".svg", ".html", ".htm", ".xml") + + def send_private_file(path: str) -> Response: path = os.path.join(frappe.local.conf.get("private_path", "private"), path.strip("/")) filename = os.path.basename(path) + extension = os.path.splitext(path)[1] + as_attachment = extension.lower() in FORCE_DOWNLOAD_EXTENSIONS + if frappe.local.request.headers.get("X-Use-X-Accel-Redirect"): path = "/protected/" + path response = Response() @@ -310,15 +316,14 @@ def send_private_file(path: str) -> Response: response.headers["Accept-Ranges"] = "bytes" response.headers["Content-Type"] = mimetypes.guess_type(filename)[0] or "application/octet-stream" + if as_attachment: + response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{quote(filename)}" + else: filepath = frappe.utils.get_site_path(path) if not os.path.exists(filepath): raise NotFound - extension = os.path.splitext(path)[1] - blacklist = [".svg", ".html", ".htm", ".xml"] - as_attachment = extension.lower() in blacklist - response = werkzeug.utils.send_file( filepath, environ=frappe.local.request.environ, From e0e69a495c7a2640ae781c2f24cd80e4d110457c Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 9 Jan 2026 19:37:08 +0530 Subject: [PATCH 2/2] fix: force download files based on extension even on development server Signed-off-by: Akhil Narang --- frappe/middlewares.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frappe/middlewares.py b/frappe/middlewares.py index 1898734917..76feb5f96b 100644 --- a/frappe/middlewares.py +++ b/frappe/middlewares.py @@ -8,12 +8,27 @@ from werkzeug.middleware.shared_data import SharedDataMiddleware import frappe from frappe.utils import cstr, get_site_name +from frappe.utils.response import FORCE_DOWNLOAD_EXTENSIONS class StaticDataMiddleware(SharedDataMiddleware): def __call__(self, environ, start_response): self.environ = environ - return super().__call__(environ, start_response) + + def patch_start_response(status, headers, exc_info=None): + if ( + (path := environ.get("PATH_INFO", "")) + and path.startswith("/files/") + and path.lower().endswith(FORCE_DOWNLOAD_EXTENSIONS) + ): + from urllib.parse import quote + + filename = Path(path).name + headers.append(("Content-Disposition", f"attachment; filename*=UTF-8''{quote(filename)}")) + + return start_response(status, headers, exc_info) + + return super().__call__(environ, patch_start_response) def get_directory_loader(self, directory): def loader(path):