Skip to content

Commit 42ce750

Browse files
fix: handle malformed YAML in update_frontmatter gracefully
Fixes #378 - Changed error handling in update_frontmatter() to match PR #368 pattern - YAML parsing errors now logged as WARNING instead of ERROR - Files with malformed frontmatter treated as plain markdown - Added test for malformed YAML case (KB: title format) This resolves 1,112 errors/3hrs in production logs from files with colons in titles that confuse the YAML parser. Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
1 parent e6c8e36 commit 42ce750

2 files changed

Lines changed: 55 additions & 9 deletions

File tree

src/basic_memory/file_utils.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
201201
202202
Raises:
203203
FileError: If file operations fail
204-
ParseError: If frontmatter parsing fails
205204
"""
206205
try:
207206
# Convert string to Path if needed
@@ -210,11 +209,20 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
210209
# Read current content
211210
content = path_obj.read_text(encoding="utf-8")
212211

213-
# Parse current frontmatter
212+
# Parse current frontmatter with error handling for malformed YAML (issue #378)
214213
current_fm = {}
215214
if has_frontmatter(content):
216-
current_fm = parse_frontmatter(content)
217-
content = remove_frontmatter(content)
215+
try:
216+
current_fm = parse_frontmatter(content)
217+
content = remove_frontmatter(content)
218+
except (ParseError, yaml.YAMLError) as e:
219+
# Log warning and treat as plain markdown (same pattern as PR #368)
220+
logger.warning(
221+
f"Failed to parse YAML frontmatter in {path_obj}: {e}. "
222+
"Treating file as plain markdown without frontmatter."
223+
)
224+
# Keep full content, treat as having no frontmatter
225+
current_fm = {}
218226

219227
# Update frontmatter
220228
new_fm = {**current_fm, **updates}
@@ -229,11 +237,13 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
229237
return await compute_checksum(final_content)
230238

231239
except Exception as e: # pragma: no cover
232-
logger.error(
233-
"Failed to update frontmatter",
234-
path=str(path) if isinstance(path, (str, Path)) else "<unknown>",
235-
error=str(e),
236-
)
240+
# Only log real errors as ERROR (not YAML parsing errors which are already handled)
241+
if not isinstance(e, (ParseError, yaml.YAMLError)):
242+
logger.error(
243+
"Failed to update frontmatter",
244+
path=str(path) if isinstance(path, (str, Path)) else "<unknown>",
245+
error=str(e),
246+
)
237247
raise FileError(f"Failed to update frontmatter: {e}")
238248

239249

tests/utils/test_file_utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,42 @@ async def test_update_frontmatter_errors(tmp_path: Path):
259259
await update_frontmatter(nonexistent, {"title": "Test"})
260260

261261

262+
@pytest.mark.asyncio
263+
async def test_update_frontmatter_malformed_yaml(tmp_path: Path):
264+
"""Test that malformed YAML frontmatter is handled gracefully (issue #378)."""
265+
test_file = tmp_path / "test.md"
266+
267+
# Create file with malformed YAML frontmatter (colon in title breaks YAML)
268+
malformed_content = """---
269+
title: KB: Something
270+
---
271+
# Test Content
272+
273+
Some content here"""
274+
test_file.write_text(malformed_content, encoding="utf-8")
275+
276+
# Update frontmatter should succeed by treating file as plain markdown
277+
updates = {"type": "note", "tags": ["test"]}
278+
checksum = await update_frontmatter(test_file, updates)
279+
280+
# Verify the update succeeded
281+
assert checksum is not None
282+
updated = test_file.read_text(encoding="utf-8")
283+
284+
# Should have new frontmatter
285+
assert "type: note" in updated
286+
assert "tags:" in updated
287+
assert "- test" in updated
288+
289+
# Original content should be preserved
290+
assert "Test Content" in updated
291+
assert "Some content here" in updated
292+
293+
# Verify the new frontmatter is valid
294+
fm = parse_frontmatter(updated)
295+
assert fm == {"type": "note", "tags": ["test"]}
296+
297+
262298
@pytest.mark.asyncio
263299
def test_sanitize_for_filename_removes_invalid_characters():
264300
# Test all invalid characters listed in the regex

0 commit comments

Comments
 (0)