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..244fbd964e9 --- /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"