Skip to content

Commit cbc1f8d

Browse files
fix: return independent metadata dicts from normalize_metadata
normalize_metadata returned the same dict object repeated across sources ([{}] * n / [meta] * n), so mutating one source's metadata leaked into all the others. Each source now receives its own independent copy. Also removes the now-redundant local workaround in FileToFileContent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c930c74 commit cbc1f8d

4 files changed

Lines changed: 29 additions & 7 deletions

File tree

haystack/components/converters/file_to_file_content.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,8 @@ def run(
8282
continue
8383

8484
base64_data = base64.b64encode(bytestream.data).decode("utf-8")
85-
# ``normalize_metadata`` returns the same dict object for every source when ``extra`` is a
86-
# single dict (or ``None``), so give each FileContent its own copy. Otherwise mutating one
87-
# file's ``extra`` downstream would leak into all the others. The other converters avoid this
88-
# implicitly by merging ``extra`` into a fresh ``{**bytestream.meta, ...}`` dict.
8985
file_content = FileContent(
90-
base64_data=base64_data, mime_type=bytestream.mime_type, filename=filename, extra=dict(extra_dict)
86+
base64_data=base64_data, mime_type=bytestream.mime_type, filename=filename, extra=extra_dict
9187
)
9288
file_contents.append(file_content)
9389

haystack/components/converters/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from copy import deepcopy
56
from pathlib import Path
67
from typing import Any
78

@@ -39,11 +40,15 @@ def normalize_metadata(meta: dict[str, Any] | list[dict[str, Any]] | None, sourc
3940
:param meta: the meta input of the converter, as-is
4041
:param sources_count: the number of sources the converter received
4142
:returns: a list of dictionaries of the make length as the sources list
43+
44+
Each source always gets its own independent dictionary. When ``meta`` is ``None`` or a single
45+
dictionary, a separate copy is returned for every source so that mutating one source's metadata
46+
downstream does not leak into the others.
4247
"""
4348
if meta is None:
44-
return [{}] * sources_count
49+
return [{} for _ in range(sources_count)]
4550
if isinstance(meta, dict):
46-
return [meta] * sources_count
51+
return [deepcopy(meta) for _ in range(sources_count)]
4752
if isinstance(meta, list):
4853
if sources_count != len(meta):
4954
raise ValueError("The length of the metadata list must match the number of sources.")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed ``normalize_metadata`` (used by all file converters) returning the same dictionary object
5+
for every source when ``meta`` is ``None`` or a single dictionary. Each source now receives an
6+
independent copy, so mutating one source's metadata downstream no longer leaks into the others.

test/components/converters/test_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ def test_normalize_metadata_single_dict():
1818
assert normalize_metadata({"a": 1}, sources_count=3) == [{"a": 1}, {"a": 1}, {"a": 1}]
1919

2020

21+
def test_normalize_metadata_none_returns_independent_dicts():
22+
result = normalize_metadata(None, sources_count=3)
23+
result[0]["file_path"] = "a.txt"
24+
assert result == [{"file_path": "a.txt"}, {}, {}]
25+
26+
27+
def test_normalize_metadata_single_dict_returns_independent_copies():
28+
meta = {"a": 1}
29+
result = normalize_metadata(meta, sources_count=3)
30+
result[0]["b"] = 2
31+
# Mutating one source's metadata must not leak into the others or the original input.
32+
assert result == [{"a": 1, "b": 2}, {"a": 1}, {"a": 1}]
33+
assert meta == {"a": 1}
34+
35+
2136
def test_normalize_metadata_list_of_right_size():
2237
assert normalize_metadata([{"a": 1}], sources_count=1) == [{"a": 1}]
2338
assert normalize_metadata([{"a": 1}, {"b": 2}, {"c": 3}], sources_count=3) == [{"a": 1}, {"b": 2}, {"c": 3}]

0 commit comments

Comments
 (0)