Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions router/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ def _atomic_write_json_sync(path: str, data) -> None:
"""Synchronously write JSON data to path using atomic temp-file + os.replace."""
os.makedirs(os.path.dirname(path), exist_ok=True)
fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(path), suffix=".tmp")
written = False
try:
try:
f = os.fdopen(fd, "w", encoding="utf-8")
Expand All @@ -634,12 +635,13 @@ def _atomic_write_json_sync(path: str, data) -> None:
with f:
json.dump(data, f, indent=2)
os.replace(tmp_path, path)
except Exception:
try:
os.unlink(tmp_path)
except Exception:
pass
raise
written = True
finally:
if not written:
try:
os.unlink(tmp_path)
except OSError:
pass


async def _atomic_write_json_async(path: str, data) -> None:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_atomic_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,35 @@ def test_atomic_write_json_sync_unlink_error(mock_unlink, mock_replace, tmp_path

# Verify target file wasn't created
assert not target_file.exists()


@patch("router.main.os.close")
@patch("router.main.os.fdopen")
def test_atomic_write_json_sync_fdopen_non_os_error(mock_fdopen, mock_close, tmp_path):
"""Test that non-OSError in fdopen closes fd and unlinks temp file without leaking."""
target_file = tmp_path / "data.json"
data = {"key": "value"}

mock_fdopen.side_effect = RuntimeError("Mocked fdopen runtime error")

with pytest.raises(RuntimeError, match="Mocked fdopen runtime error"):
_atomic_write_json_sync(str(target_file), data)

assert mock_close.called
assert not target_file.exists()
assert list(tmp_path.iterdir()) == []


@patch("router.main.json.dump")
def test_atomic_write_json_sync_unexpected_exception_cleanup(mock_dump, tmp_path):
"""Test cleanup on unexpected exception types (e.g. RecursionError)."""
target_file = tmp_path / "data.json"
data = {"key": "value"}

mock_dump.side_effect = RecursionError("Mocked recursion limit")

with pytest.raises(RecursionError, match="Mocked recursion limit"):
_atomic_write_json_sync(str(target_file), data)

assert not target_file.exists()
assert list(tmp_path.iterdir()) == []