From 37d6fdcb493c6e79c5ec10079fadf720c978b903 Mon Sep 17 00:00:00 2001 From: Gyanu Mayank Date: Wed, 16 Sep 2026 08:42:53 +0530 Subject: [PATCH] fix: keep file delete when secondary storage returns 403 User-initiated delete already dropped the DB row after a failed S3 unlink. The code-env / local-disk pass still aborted, so a 403 from the extra store left the row in place. Fixes #15971 --- api/server/services/Files/process.js | 13 +++++++- api/server/services/Files/process.spec.js | 38 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 05b02ae6df6..05c37a556e5 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -211,7 +211,18 @@ const createDeleteFileWithSecondaryStorage = ({ source, deleteFile, deletionMeth } await Promise.all( - secondaryDeleteMethods.map((secondaryDeleteFile) => secondaryDeleteFile(req, file)), + secondaryDeleteMethods.map((secondaryDeleteFile) => + secondaryDeleteFile(req, file).catch((err) => { + if (isMissingStorageError(err)) { + logger.warn('Secondary file storage was already missing during delete', err); + return; + } + logger.warn( + 'Secondary file storage delete failed; continuing so the file record can still be removed', + err, + ); + }), + ), ); }; }; diff --git a/api/server/services/Files/process.spec.js b/api/server/services/Files/process.spec.js index efbb8a1b008..ff8911aff03 100644 --- a/api/server/services/Files/process.spec.js +++ b/api/server/services/Files/process.spec.js @@ -2703,6 +2703,44 @@ describe('processDeleteRequest', () => { expect(db.deleteFiles).toHaveBeenCalledWith(['code-resource-file']); expect(result).toEqual({ deletedFileIds: ['code-resource-file'], failedFileIds: [] }); }); + + it('still removes metadata when secondary code-env delete fails permanently', async () => { + const primaryDelete = jest.fn().mockResolvedValue(undefined); + const codeDelete = jest + .fn() + .mockRejectedValue(Object.assign(new Error('session gone'), { status: 403 })); + getStrategyFunctions.mockImplementation((source) => + source === FileSources.execute_code + ? { deleteFile: codeDelete } + : { deleteFile: primaryDelete }, + ); + db.deleteFiles.mockResolvedValue({ deletedCount: 1 }); + const req = { + body: {}, + config: {}, + user: { id: 'user-123', tenantId: 'tenant-a' }, + }; + const file = { + file_id: 'code-resource-file', + filepath: '/uploads/code-resource.txt', + source: FileSources.local, + metadata: { + codeEnvRef: { + kind: 'agent', + id: 'agent-abc', + storage_session_id: 'sess-1', + file_id: 'fid-1', + }, + }, + }; + + const result = await processDeleteRequest({ req, files: [file] }); + + expect(primaryDelete).toHaveBeenCalledWith(req, file, undefined); + expect(codeDelete).toHaveBeenCalledWith(req, file); + expect(db.deleteFiles).toHaveBeenCalledWith(['code-resource-file']); + expect(result).toEqual({ deletedFileIds: ['code-resource-file'], failedFileIds: [] }); + }); }); describe('sweepExpiredFiles', () => {