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..026b2c35 100644 --- a/test/storage/utils/object.test.js +++ b/test/storage/utils/object.test.js @@ -64,4 +64,29 @@ describe('Storage Object Utils tests', () => { assert.strictEqual(called.length, 1); assert.strictEqual(called[0], 'https://localhost/api/v1/syncadmin?doc=https://admin.da.live/source/a/b/c.html'); }); + + it('Should not throw when collab returns a null-body response (e.g. 204)', async () => { + // Reproduces the case where da-collab's deleteAdmin handler returns + // `new Response(null, { status: 204 })` — common when a doc is open in + // another window and gets invalidated. In that case `resp.body` is null, + // and calling `.cancel()` on it would throw if not optional-chained. + const called = []; + const env = { + dacollab: { + fetch: async (url) => { + called.push(url); + // Mirror the shape of a Workers Response with a 204 status: + // body is null, so resp.body.cancel() would TypeError without `?.`. + return { status: 204, body: null }; + }, + }, + }; + + await assert.doesNotReject( + () => notifyCollab('deleteadmin', 'https://admin.da.live/source/a/b/c.html', env), + 'notifyCollab must tolerate a null response body', + ); + assert.strictEqual(called.length, 1); + assert.strictEqual(called[0], 'https://localhost/api/v1/deleteadmin?doc=https://admin.da.live/source/a/b/c.html'); + }); });