From 67448ca9a187ea0c3104c7590e2fd6bacaa00f30 Mon Sep 17 00:00:00 2001 From: WalkingDreams798 Date: Sat, 20 Jun 2026 01:52:22 +0700 Subject: [PATCH] fix(tools): raise a graceful ToolError when a memory file is not valid UTF-8 `view` / `str_replace` / `insert` read memory files via `_read_file_content`, which decodes strict UTF-8 and only caught `FileNotFoundError`. A memory file containing non-UTF-8 bytes therefore raised an uncaught `UnicodeDecodeError` out of the tool (platform-independent, since the encoding is hard-coded to utf-8), instead of a normalized `ToolError` like every other failure mode. Catch `UnicodeDecodeError` in both the sync and async readers and surface it as a `ToolError`. Added sync + async regression tests reading a binary file. --- .../lib/tools/_beta_builtin_memory_tool.py | 4 ++++ .../lib/tools/memory_tools/test_filesystem.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/anthropic/lib/tools/_beta_builtin_memory_tool.py b/src/anthropic/lib/tools/_beta_builtin_memory_tool.py index edb948a26..f5b4620ad 100644 --- a/src/anthropic/lib/tools/_beta_builtin_memory_tool.py +++ b/src/anthropic/lib/tools/_beta_builtin_memory_tool.py @@ -327,6 +327,8 @@ def _read_file_content(full_path: Path, memory_path: str) -> str: raise ToolError( f"The file {memory_path} no longer exists (may have been deleted or renamed concurrently)." ) from err + except UnicodeDecodeError as err: + raise ToolError(f"The file {memory_path} is not a valid UTF-8 text file and cannot be read as memory.") from err def _format_file_size(bytes_size: int) -> str: @@ -620,6 +622,8 @@ async def _async_read_file_content(full_path: AsyncPath, memory_path: str) -> st raise ToolError( f"The file {memory_path} no longer exists (may have been deleted or renamed concurrently)." ) from err + except UnicodeDecodeError as err: + raise ToolError(f"The file {memory_path} is not a valid UTF-8 text file and cannot be read as memory.") from err class BetaAsyncLocalFilesystemMemoryTool(BetaAsyncAbstractMemoryTool): diff --git a/tests/lib/tools/memory_tools/test_filesystem.py b/tests/lib/tools/memory_tools/test_filesystem.py index 9a9398f40..cbfffe26f 100644 --- a/tests/lib/tools/memory_tools/test_filesystem.py +++ b/tests/lib/tools/memory_tools/test_filesystem.py @@ -164,6 +164,16 @@ def test_view_error_for_non_existent_file(self, sync_local_filesystem_tool: Beta BetaMemoryTool20250818ViewCommand(command="view", path="/memories/nonexistent.txt") ) + def test_view_error_for_non_utf8_file(self, sync_local_filesystem_tool: BetaLocalFilesystemMemoryTool) -> None: + memory_root = sync_local_filesystem_tool.memory_root + memory_root.mkdir(parents=True, exist_ok=True) + (memory_root / "binary.dat").write_bytes(b"\xff\xfe\x80 not utf-8") + + with pytest.raises(ToolError, match="is not a valid UTF-8 text file"): + sync_local_filesystem_tool.view( + BetaMemoryTool20250818ViewCommand(command="view", path="/memories/binary.dat") + ) + def test_view_error_for_files_with_too_many_lines( self, sync_local_filesystem_tool: BetaLocalFilesystemMemoryTool ) -> None: @@ -635,6 +645,18 @@ async def test_view_error_for_non_existent_file( BetaMemoryTool20250818ViewCommand(command="view", path="/memories/nonexistent.txt") ) + async def test_view_error_for_non_utf8_file( + self, async_local_filesystem_tool: BetaAsyncLocalFilesystemMemoryTool + ) -> None: + memory_root = Path(str(async_local_filesystem_tool.memory_root)) + memory_root.mkdir(parents=True, exist_ok=True) + (memory_root / "binary.dat").write_bytes(b"\xff\xfe\x80 not utf-8") + + with pytest.raises(ToolError, match="is not a valid UTF-8 text file"): + await async_local_filesystem_tool.view( + BetaMemoryTool20250818ViewCommand(command="view", path="/memories/binary.dat") + ) + async def test_view_error_for_files_with_too_many_lines( self, async_local_filesystem_tool: BetaAsyncLocalFilesystemMemoryTool ) -> None: