From a3453e78ec394b71a4a4efd8e0ef8c607ce0955b Mon Sep 17 00:00:00 2001 From: Alejandro Torres Date: Mon, 19 Oct 2026 16:28:10 -0300 Subject: [PATCH] [FIX] fs_attachment: don't crash on fs streams without mimetype For "fs" streams, the mimetype is taken as is from the attachment and may be empty for attachments written outside the ORM. Since #584, ir.binary._get_stream_from then called guess_extension(False), and FsStream.get_response passed mimetype=False to werkzeug: both raised an AttributeError and the download returned a 500. Fall back to default_mimetype for fs streams with no mimetype, as core does for "data" and "path" streams, and only run the extension guess for fs streams, since core already does it for the other stream types. Fixes #684 Co-Authored-By: Claude Opus 5.5 --- fs_attachment/models/ir_binary.py | 13 ++--- .../readme/newsfragments/684.bugfix.rst | 1 + fs_attachment/tests/test_stream.py | 48 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 fs_attachment/readme/newsfragments/684.bugfix.rst diff --git a/fs_attachment/models/ir_binary.py b/fs_attachment/models/ir_binary.py index d9ea167d02..a28faa2d1b 100644 --- a/fs_attachment/models/ir_binary.py +++ b/fs_attachment/models/ir_binary.py @@ -70,16 +70,17 @@ def _get_stream_from( if stream.type == "fs": if mimetype: stream.mimetype = mimetype + elif not stream.mimetype: + stream.mimetype = default_mimetype if filename: stream.download_name = filename elif record and filename_field in record: stream.download_name = record[filename_field] or stream.download_name - - if ( - not get_extension(stream.download_name) - and stream.mimetype != "application/octet-stream" - ): - stream.download_name += guess_extension(stream.mimetype) or "" + if ( + not get_extension(stream.download_name) + and stream.mimetype != "application/octet-stream" + ): + stream.download_name += guess_extension(stream.mimetype) or "" return stream diff --git a/fs_attachment/readme/newsfragments/684.bugfix.rst b/fs_attachment/readme/newsfragments/684.bugfix.rst new file mode 100644 index 0000000000..554605b859 --- /dev/null +++ b/fs_attachment/readme/newsfragments/684.bugfix.rst @@ -0,0 +1 @@ +Fix a 500 error when downloading an attachment stored in a filesystem storage that has no mimetype: fall back to the default mimetype, as core does for attachments in the filestore. diff --git a/fs_attachment/tests/test_stream.py b/fs_attachment/tests/test_stream.py index 38ee7a4c0f..1a1abbe79d 100644 --- a/fs_attachment/tests/test_stream.py +++ b/fs_attachment/tests/test_stream.py @@ -240,3 +240,51 @@ def test_image_url_name_with_newline(self): }, assert_content=self.image, ) + + def _create_attachment_without_extension(self): + return ( + self.env["ir.attachment"] + .with_context(storage_location=self.temp_backend.code) + .create({"name": "report", "raw": self.content}) + ) + + def test_content_url_name_without_extension(self): + """The extension is guessed from the mimetype for fs streams.""" + attachment = self._create_attachment_without_extension() + self.assertEqual(attachment.mimetype, "text/plain") + self.authenticate("admin", "admin") + self.assertDownload( + f"/web/content/{attachment.id}", + headers={}, + assert_status_code=200, + assert_headers={"Content-Disposition": "inline; filename=report.txt"}, + assert_content=self.content, + ) + + def test_content_url_without_mimetype(self): + """An fs attachment without mimetype can still be downloaded. + + The mimetype can be empty for attachments written outside the ORM + (imports, migrations). As in core, it falls back to the default + mimetype instead of crashing. + """ + attachment = self._create_attachment_without_extension() + self.env.cr.execute( + "UPDATE ir_attachment SET mimetype = NULL WHERE id = %s", (attachment.id,) + ) + attachment.invalidate_recordset(["mimetype"]) + stream = self.env["ir.binary"]._get_stream_from(attachment) + self.assertEqual(stream.type, "fs") + self.assertEqual(stream.mimetype, "application/octet-stream") + self.assertEqual(stream.download_name, "report") + self.authenticate("admin", "admin") + self.assertDownload( + f"/web/content/{attachment.id}", + headers={}, + assert_status_code=200, + assert_headers={ + "Content-Type": "application/octet-stream", + "Content-Disposition": "inline; filename=report", + }, + assert_content=self.content, + )