From 119003668413afa0971207c2d01f1a8dfbe62c71 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 29 Apr 2026 15:16:38 +0200 Subject: [PATCH] fix: guard against null body in notifyCollab to prevent move partial_failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit notifyCollab called resp.body.cancel() unconditionally. When the collab service returns a bodyless response, this threw TypeError in the finally block of copyFile, cancelling the successful copy return value and causing moves to fail with partial_failure 500 — leaving files duplicated at both source and destination instead of being renamed. Co-Authored-By: Claude Sonnet 4.6 --- src/storage/utils/object.js | 2 +- test/storage/utils/object.test.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/storage/utils/object.js b/src/storage/utils/object.js index 18ca3e36..fc5367da 100644 --- a/src/storage/utils/object.js +++ b/src/storage/utils/object.js @@ -35,5 +35,5 @@ export async function notifyCollab(api, url, env) { // method: 'POST', headers, }); - resp.body.cancel(); + resp.body?.cancel(); } diff --git a/test/storage/utils/object.test.js b/test/storage/utils/object.test.js index 4606f34f..3ce432ff 100644 --- a/test/storage/utils/object.test.js +++ b/test/storage/utils/object.test.js @@ -46,6 +46,22 @@ describe('Storage Object Utils tests', () => { assert.strictEqual(called.length, 0, 'should not have invalidated anything'); }); + it('does not throw when collab response has null body', async () => { + // Regression test: notifyCollab calls resp.body.cancel() unconditionally. When the collab + // service returns a response with no body (null), this throws TypeError: Cannot read + // properties of null (reading 'cancel'). In copyFile this fires in a finally block, + // which cancels the return value and causes the move to fail with partial_failure 500 — + // leaving the file duplicated at both source and destination. + const env = { + dacollab: { + fetch: async () => ({ body: null }), + }, + }; + await assert.doesNotReject( + notifyCollab('syncadmin', 'https://admin.da.live/source/a/b/c.html', env), + ); + }); + it('Should invalidate (with shared secret', async () => { const called = []; const env = {