From 024cfcfbb2270529e8a26341d588e7c5c6f6b038 Mon Sep 17 00:00:00 2001 From: Vedant Agarwal Date: Sun, 5 Jul 2026 18:33:31 +0530 Subject: [PATCH 1/2] fix: give each FileContent its own extra dict in FileToFileContent normalize_metadata returns the same dict object for every source when extra is a single dict (or None). FileToFileContent passed that shared object straight into each FileContent, so mutating one file's extra downstream leaked into all the others. Copy the dict per source, matching the other converters which merge extra into a fresh dict. Co-Authored-By: Claude Opus 4.8 --- .../components/converters/file_to_file_content.py | 6 +++++- ...file-content-shared-extra-4d0f1d099ab3d226.yaml | 6 ++++++ .../converters/test_file_to_file_content.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml diff --git a/haystack/components/converters/file_to_file_content.py b/haystack/components/converters/file_to_file_content.py index 1e3b9c7d1fb..2a66a606bbf 100644 --- a/haystack/components/converters/file_to_file_content.py +++ b/haystack/components/converters/file_to_file_content.py @@ -82,8 +82,12 @@ def run( continue base64_data = base64.b64encode(bytestream.data).decode("utf-8") + # ``normalize_metadata`` returns the same dict object for every source when ``extra`` is a + # single dict (or ``None``), so give each FileContent its own copy. Otherwise mutating one + # file's ``extra`` downstream would leak into all the others. The other converters avoid this + # implicitly by merging ``extra`` into a fresh ``{**bytestream.meta, ...}`` dict. file_content = FileContent( - base64_data=base64_data, mime_type=bytestream.mime_type, filename=filename, extra=extra_dict + base64_data=base64_data, mime_type=bytestream.mime_type, filename=filename, extra=dict(extra_dict) ) file_contents.append(file_content) diff --git a/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml b/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml new file mode 100644 index 00000000000..28e31e2873a --- /dev/null +++ b/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed `FileToFileContent` sharing a single `extra` dictionary across all produced `FileContent` + objects when `extra` is passed as one dict (or left as `None`). Each `FileContent` now gets its + own copy, so mutating one file's `extra` no longer leaks into the others. diff --git a/test/components/converters/test_file_to_file_content.py b/test/components/converters/test_file_to_file_content.py index 7476b02df3f..c3370b29f28 100644 --- a/test/components/converters/test_file_to_file_content.py +++ b/test/components/converters/test_file_to_file_content.py @@ -113,6 +113,20 @@ def test_run_with_extra_list(self) -> None: assert results["file_contents"][0].extra == {"key": "value1"} assert results["file_contents"][1].extra == {"key": "value2"} + def test_run_with_extra_dict_does_not_share_reference(self) -> None: + # A single ``extra`` dict is applied to every source; each FileContent must get its own copy + # so that mutating one file's ``extra`` downstream does not leak into the others. + converter = FileToFileContent() + sources = ["./test/test_files/txt/doc_1.txt", "./test/test_files/txt/doc_2.txt"] + results = converter.run(sources=sources, extra={"tenant": "acme"}) + + file_contents = results["file_contents"] + assert len(file_contents) == 2 + assert file_contents[0].extra is not file_contents[1].extra + + file_contents[0].extra["page"] = 1 + assert "page" not in file_contents[1].extra + def test_run_skips_empty_files_among_valid(self, caplog) -> None: byte_stream_empty = ByteStream(data=b"") valid_source = "./test/test_files/txt/doc_1.txt" From b6b84e821b3f053b8bb14c1fc0f2735b7ce23666 Mon Sep 17 00:00:00 2001 From: Vedant Agarwal Date: Wed, 8 Jul 2026 15:36:47 +0530 Subject: [PATCH 2/2] docs: use double backticks for inline code in release note Co-Authored-By: Claude Opus 4.8 --- ...-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml b/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml index 28e31e2873a..244fbd964e9 100644 --- a/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml +++ b/releasenotes/notes/fix-file-to-file-content-shared-extra-4d0f1d099ab3d226.yaml @@ -1,6 +1,6 @@ --- fixes: - | - Fixed `FileToFileContent` sharing a single `extra` dictionary across all produced `FileContent` - objects when `extra` is passed as one dict (or left as `None`). Each `FileContent` now gets its - own copy, so mutating one file's `extra` no longer leaks into the others. + Fixed ``FileToFileContent`` sharing a single ``extra`` dictionary across all produced ``FileContent`` + objects when ``extra`` is passed as one dict (or left as ``None``). Each ``FileContent`` now gets its + own copy, so mutating one file's ``extra`` no longer leaks into the others.