From f1286275ab1d9d5acc745a6eac54c03672739b1d Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 6 Jul 2026 11:13:15 +0200 Subject: [PATCH] fix(cloudflare): Catch potential errors during flush and dispose --- packages/cloudflare/src/flush.ts | 29 ++++++++++++++++++------- packages/cloudflare/test/flush.test.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/cloudflare/src/flush.ts b/packages/cloudflare/src/flush.ts index 6dc7ade0d45f..5d509b46103b 100644 --- a/packages/cloudflare/src/flush.ts +++ b/packages/cloudflare/src/flush.ts @@ -1,6 +1,7 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import type { Client } from '@sentry/core'; -import { flush } from '@sentry/core'; +import { debug, flush } from '@sentry/core'; +import { DEBUG_BUILD } from './debug-build'; type FlushLock = { readonly ready: Promise; @@ -118,17 +119,29 @@ function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegis * Flushes the client and then disposes of it to allow garbage collection. * This should be called at the end of each request to prevent memory leaks. * + * This function never rejects. On Workers, a rejected promise passed to + * `ctx.waitUntil` marks the whole invocation as `outcome: exception` even when + * the handler itself completed successfully. Since flush/dispose is internal + * SDK housekeeping, a failure here must not fail the user's invocation. + * * @param client - The CloudflareClient instance to flush and dispose * @param timeout - Timeout in milliseconds for the flush operation * @returns A promise that resolves when flush and dispose are complete */ export async function flushAndDispose(client: Client | undefined, timeout = 2000): Promise { - if (!client) { - await flush(timeout); - - return; + try { + if (!client) { + await flush(timeout); + return; + } + await client.flush(timeout); + } catch (e) { + DEBUG_BUILD && debug.warn('Failed to flush client', e); + } finally { + try { + client?.dispose(); + } catch (e) { + DEBUG_BUILD && debug.warn('Failed to dispose client', e); + } } - - await client.flush(timeout); - client.dispose(); } diff --git a/packages/cloudflare/test/flush.test.ts b/packages/cloudflare/test/flush.test.ts index a0b4de60bb77..49ce15dc5153 100644 --- a/packages/cloudflare/test/flush.test.ts +++ b/packages/cloudflare/test/flush.test.ts @@ -108,6 +108,36 @@ describe('flushAndDispose', () => { expect(flushSpy).toHaveBeenCalled(); flushSpy.mockRestore(); }); + + it('should not reject when client.flush rejects', async () => { + const mockClient = { + flush: vi.fn().mockRejectedValue(new Error('flush failed')), + dispose: vi.fn(), + } as unknown as Client; + + await expect(flushAndDispose(mockClient)).resolves.toBeUndefined(); + // dispose re-arms the client (recreates its transport) for the next isolate + // invocation, so it must still run even when flush failed. + expect(mockClient.dispose).toHaveBeenCalled(); + }); + + it('should not reject when client.dispose throws', async () => { + const mockClient = { + flush: vi.fn().mockResolvedValue(true), + dispose: vi.fn().mockImplementation(() => { + throw new Error('dispose failed'); + }), + } as unknown as Client; + + await expect(flushAndDispose(mockClient)).resolves.toBeUndefined(); + }); + + it('should not reject when the global flush rejects', async () => { + const flushSpy = vi.spyOn(sentryCore, 'flush').mockRejectedValue(new Error('flush failed')); + + await expect(flushAndDispose(undefined)).resolves.toBeUndefined(); + flushSpy.mockRestore(); + }); }); describe('getOriginalWaitUntil', () => {