diff --git a/backend/src/baserow/contrib/automation/nodes/handler.py b/backend/src/baserow/contrib/automation/nodes/handler.py index 8c3756a9f6..dca96d342f 100644 --- a/backend/src/baserow/contrib/automation/nodes/handler.py +++ b/backend/src/baserow/contrib/automation/nodes/handler.py @@ -424,12 +424,27 @@ def dispatch_node( logger.error(str(e)) return None - node = self.get_node(node_id) - simulate_until_node = ( - node.workflow.get_graph().get_node(workflow_history.simulate_until_node_id) - if workflow_history.simulate_until_node_id - else None + error = ( + "Node with ID {} was not found. The node was likely " + "deleted before the task was executed." ) + try: + node = self.get_node(node_id) + except AutomationNodeDoesNotExist: + logger.warning(error.format(node_id)) + return None + + try: + simulate_until_node = ( + node.workflow.get_graph().get_node( + workflow_history.simulate_until_node_id + ) + if workflow_history.simulate_until_node_id + else None + ) + except AutomationNodeDoesNotExist: + logger.warning(error.format(workflow_history.simulate_until_node_id)) + return None if simulate_until_node: allowed_nodes = { diff --git a/backend/tests/baserow/contrib/automation/nodes/test_node_dispatch_async.py b/backend/tests/baserow/contrib/automation/nodes/test_node_dispatch_async.py index af3d69c695..a7944cf8c7 100644 --- a/backend/tests/baserow/contrib/automation/nodes/test_node_dispatch_async.py +++ b/backend/tests/baserow/contrib/automation/nodes/test_node_dispatch_async.py @@ -1456,3 +1456,29 @@ def test_dispatch_node_iterator_with_no_rows(data_fixture): # Ensure we never return an empty chain, which would cause # self.replace() to crash with an error. assert result is None + + +@pytest.mark.django_db +@patch(f"{NODE_HANDLER_PATH}.logger") +def test_dispatch_node_with_deleted_node(mock_logger, data_fixture): + """ + In the rare case where a node is deleted between the time a dispatch + is queued and when the task actually runs, we should handle this + gracefully instead of crashing. + """ + + data = create_workflow(data_fixture) + action_node = data["action_node"] + history = data["workflow_history"] + + # delete the node to simulate a race condition + action_node_id = action_node.id + action_node.delete() + + result = AutomationNodeHandler().dispatch_node(action_node_id, history.id) + assert result is None + expected_error = ( + f"Node with ID {action_node_id} was not found. The node was likely " + "deleted before the task was executed." + ) + mock_logger.warning.assert_called_once_with(expected_error) diff --git a/changelog/entries/unreleased/bug/5140_fix_import_workspace_crashes_on_retry_after_failed_backend_i.json b/changelog/entries/unreleased/bug/5140_fix_import_workspace_crashes_on_retry_after_failed_backend_i.json new file mode 100644 index 0000000000..d452dc134d --- /dev/null +++ b/changelog/entries/unreleased/bug/5140_fix_import_workspace_crashes_on_retry_after_failed_backend_i.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fix import workspace crashes on retry after failed backend import", + "issue_origin": "github", + "issue_number": 5140, + "domain": "core", + "bullet_points": [], + "created_at": "2026-04-07" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/fixed_a_bug_that_caused_a_crash_due_to_a_race_condition_that.json b/changelog/entries/unreleased/bug/fixed_a_bug_that_caused_a_crash_due_to_a_race_condition_that.json new file mode 100644 index 0000000000..8cb789fa1b --- /dev/null +++ b/changelog/entries/unreleased/bug/fixed_a_bug_that_caused_a_crash_due_to_a_race_condition_that.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixed a bug that caused a crash due to a race condition that could happen if a node is deleted while it is being dispatched.", + "issue_origin": "github", + "issue_number": null, + "domain": "automation", + "bullet_points": [], + "created_at": "2026-03-26" +} \ No newline at end of file diff --git a/web-frontend/modules/core/components/import/ImportWorkspaceForm.vue b/web-frontend/modules/core/components/import/ImportWorkspaceForm.vue deleted file mode 100644 index 8f90d386ad..0000000000 --- a/web-frontend/modules/core/components/import/ImportWorkspaceForm.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/web-frontend/modules/core/components/import/ImportWorkspaceModal.vue b/web-frontend/modules/core/components/import/ImportWorkspaceModal.vue index cdaaa4d246..9ed8f7d610 100644 --- a/web-frontend/modules/core/components/import/ImportWorkspaceModal.vue +++ b/web-frontend/modules/core/components/import/ImportWorkspaceModal.vue @@ -92,8 +92,6 @@ /> - -
{{ $t('importWorkspaceModal.import') }} @@ -134,7 +132,6 @@ import { mimetype2icon } from '@baserow/modules/core/utils/fileTypeToIcon' import job from '@baserow/modules/core/mixins/job' import modal from '@baserow/modules/core/mixins/modal' import error from '@baserow/modules/core/mixins/error' -import ImportWorkspaceForm from '@baserow/modules/core/components/import/ImportWorkspaceForm.vue' import { notifyIf } from '@baserow/modules/core/utils/error' import { ImportApplicationsJobType } from '@baserow/modules/core/jobTypes' import { ResponseErrorMessage } from '@baserow/modules/core/plugins/clientHandler' @@ -161,7 +158,6 @@ export default { components: { UploadFileDropzone, SelectedFileDetails, - ImportWorkspaceForm, ImportApplicationSelector, }, mixins: [modal, error, job], @@ -217,9 +213,6 @@ export default { }, }, methods: { - submitForm() { - this.$refs.form.submit() - }, show(...args) { this.hideError() this.checkPendingImport()