From 69ef3643dc21b27852f3f555f51c6049a33d5a6f Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Tue, 20 Jan 2026 16:38:14 +0100 Subject: [PATCH] Remove upload id from logs and handle upload clear too. --- .../mergin/sync/public_api_v2_controller.py | 12 +++--- server/mergin/tests/test_public_api_v2.py | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py index 76fb5daa..2b7f124e 100644 --- a/server/mergin/sync/public_api_v2_controller.py +++ b/server/mergin/sync/public_api_v2_controller.py @@ -302,6 +302,7 @@ def create_project_version(id): upload.clear() return DataSyncError(failed_files=errors).response(422) + upload_deleted = False try: pv = ProjectVersion( project, @@ -330,7 +331,7 @@ def create_project_version(id): remove_transaction_chunks.delay(chunks_ids) logging.info( - f"Push finished for project: {project.id}, project version: {v_next_version}, upload id: {upload.id}." + f"Push finished for project: {project.id}, project version: {v_next_version}." ) project_version_created.send(pv) push_finished.send(pv) @@ -341,9 +342,9 @@ def create_project_version(id): ObjectDeletedError, ) as err: db.session.rollback() + upload_deleted = isinstance(err, ObjectDeletedError) logging.exception( - f"Failed to finish push for project: {project.id}, project version: {v_next_version}, " - f"upload id: {upload.id}.: {str(err)}" + f"Failed to finish push for project: {project.id}, project version: {v_next_version}: {str(err)}" ) if ( os.path.exists(version_dir) @@ -365,8 +366,9 @@ def create_project_version(id): move_to_tmp(version_dir) raise finally: - # remove artifacts - upload.clear() + # remove artifacts only if upload object is still valid + if not upload_deleted: + upload.clear() result = ProjectSchemaV2().dump(project) result["files"] = ProjectFileSchema( diff --git a/server/mergin/tests/test_public_api_v2.py b/server/mergin/tests/test_public_api_v2.py index e058c589..6e702f31 100644 --- a/server/mergin/tests/test_public_api_v2.py +++ b/server/mergin/tests/test_public_api_v2.py @@ -20,6 +20,7 @@ import shutil from unittest.mock import patch from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm.exc import ObjectDeletedError import pytest from datetime import datetime, timedelta, timezone import json @@ -485,6 +486,46 @@ def test_create_version_failures(client): assert response.status_code == 409 +def test_create_version_object_deleted_error(client): + """Test that ObjectDeletedError during push returns 422 without secondary exception""" + project = Project.query.filter_by( + workspace_id=test_workspace_id, name=test_project + ).first() + + data = { + "version": "v1", + "changes": { + "added": [], + "removed": [ + file_info(test_project_dir, "base.gpkg"), + ], + "updated": [], + }, + } + + # Create a real ObjectDeletedError by using internal SQLAlchemy state + def raise_object_deleted(*args, **kwargs): + # Create a minimal state-like object that ObjectDeletedError can use + class FakeState: + class_ = Upload + + def obj(self): + return None + + raise ObjectDeletedError(FakeState()) + + with patch.object( + ProjectVersion, + "__init__", + side_effect=raise_object_deleted, + ): + response = client.post(f"v2/projects/{project.id}/versions", json=data) + + # Should return 422 UploadError, not 500 from secondary exception + assert response.status_code == 422 + assert response.json["code"] == UploadError.code + + def test_upload_chunk(client): """Test pushing a chunk to a project""" project = Project.query.filter_by(