From e2ab579773ec0ea58b3cc81ccb7da5ee4a5cf8fd Mon Sep 17 00:00:00 2001 From: saltbo Date: Sat, 11 Apr 2026 23:52:12 -0400 Subject: [PATCH] fix(s3): replace DeleteObjectsCommand with per-key deletes for CF Workers compat DeleteObjectsCommand returns XML that requires DOMParser to parse, but Cloudflare Workers doesn't have DOMParser. Use parallel deleteObject calls instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/services/s3.test.ts | 14 ++++---------- server/services/s3.ts | 11 +++-------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/server/services/s3.test.ts b/server/services/s3.test.ts index 3f4bb1fd..33b6944b 100644 --- a/server/services/s3.test.ts +++ b/server/services/s3.test.ts @@ -204,17 +204,11 @@ describe('S3Service', () => { }) describe('deleteObjects', () => { - it('sends DeleteObjectsCommand with keys', async () => { - mockSend.mockResolvedValueOnce({ $metadata: {} }) + it('sends individual DeleteObjectCommand for each key', async () => { + mockSend.mockResolvedValue({ $metadata: {} }) await service.deleteObjects(storage, ['a.jpg', 'b.jpg']) - expect(mockSend).toHaveBeenCalledWith( - expect.objectContaining({ - input: { - Bucket: 'my-bucket', - Delete: { Objects: [{ Key: 'a.jpg' }, { Key: 'b.jpg' }] }, - }, - }), - ) + expect(mockSend).toHaveBeenCalledWith(expect.objectContaining({ input: { Bucket: 'my-bucket', Key: 'a.jpg' } })) + expect(mockSend).toHaveBeenCalledWith(expect.objectContaining({ input: { Bucket: 'my-bucket', Key: 'b.jpg' } })) }) it('skips API call for empty keys array', async () => { diff --git a/server/services/s3.ts b/server/services/s3.ts index 193ac544..c3a1b24e 100644 --- a/server/services/s3.ts +++ b/server/services/s3.ts @@ -1,7 +1,6 @@ import { CopyObjectCommand, DeleteObjectCommand, - DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, @@ -89,12 +88,8 @@ export class S3Service { async deleteObjects(storage: Storage, keys: string[]): Promise { if (keys.length === 0) return - const client = this.createClient(storage) - await client.send( - new DeleteObjectsCommand({ - Bucket: storage.bucket, - Delete: { Objects: keys.map((Key) => ({ Key })) }, - }), - ) + // DeleteObjectsCommand returns XML which requires DOMParser to parse. + // Cloudflare Workers doesn't have DOMParser, so we delete one-by-one. + await Promise.all(keys.map((key) => this.deleteObject(storage, key))) } }