From 9a155ee6a3dd54ea6909c19ee4219e04237f8379 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 17 Dec 2025 14:20:50 +0100 Subject: [PATCH] fix(cloudflare): Consume body of fetch in the Cloudflare transport --- packages/cloudflare/src/transport.ts | 13 +++- packages/cloudflare/test/transport.test.ts | 72 ++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/transport.ts b/packages/cloudflare/src/transport.ts index 8881e2dd6567..8e0e82aae7e0 100644 --- a/packages/cloudflare/src/transport.ts +++ b/packages/cloudflare/src/transport.ts @@ -89,7 +89,18 @@ export function makeCloudflareTransport(options: CloudflareTransportOptions): Tr }; return suppressTracing(() => { - return (options.fetch ?? fetch)(options.url, requestOptions).then(response => { + return (options.fetch ?? fetch)(options.url, requestOptions).then(async response => { + // Consume the response body to satisfy Cloudflare Workers' fetch requirements. + // The runtime requires all fetch response bodies to be read or explicitly canceled + // to prevent connection stalls and potential deadlocks. We read the body as text + // even though we don't use the content, as Sentry's response information is in the headers. + // See: https://github.com/getsentry/sentry-javascript/issues/18534 + try { + await response.text(); + } catch { + // no-op + } + return { statusCode: response.status, headers: { diff --git a/packages/cloudflare/test/transport.test.ts b/packages/cloudflare/test/transport.test.ts index 71b231f542af..fdb9fbc5e30f 100644 --- a/packages/cloudflare/test/transport.test.ts +++ b/packages/cloudflare/test/transport.test.ts @@ -106,6 +106,78 @@ describe('Edge Transport', () => { ...REQUEST_OPTIONS, }); }); + + describe('Response body consumption (issue #18534)', () => { + it('consumes the response body to prevent Cloudflare stalled connection warnings', async () => { + const textMock = vi.fn(() => Promise.resolve('OK')); + const headers = { + get: vi.fn(), + }; + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + headers, + status: 200, + text: textMock, + }), + ); + + const transport = makeCloudflareTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); + + await transport.send(ERROR_ENVELOPE); + await transport.flush(); + + expect(textMock).toHaveBeenCalledTimes(1); + expect(headers.get).toHaveBeenCalledTimes(2); + expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(headers.get).toHaveBeenCalledWith('Retry-After'); + }); + + it('handles response body consumption errors gracefully', async () => { + const textMock = vi.fn(() => Promise.reject(new Error('Body read error'))); + const headers = { + get: vi.fn(), + }; + + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + headers, + status: 200, + text: textMock, + }), + ); + + const transport = makeCloudflareTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); + + await expect(transport.send(ERROR_ENVELOPE)).resolves.toBeDefined(); + await expect(transport.flush()).resolves.toBeDefined(); + + expect(textMock).toHaveBeenCalledTimes(1); + expect(headers.get).toHaveBeenCalledTimes(2); + expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(headers.get).toHaveBeenCalledWith('Retry-After'); + }); + + it('handles a potential never existing use case of a non existing text method', async () => { + const headers = { + get: vi.fn(), + }; + + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + headers, + status: 200, + }), + ); + + const transport = makeCloudflareTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); + + await expect(transport.send(ERROR_ENVELOPE)).resolves.toBeDefined(); + await expect(transport.flush()).resolves.toBeDefined(); + expect(headers.get).toHaveBeenCalledTimes(2); + expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(headers.get).toHaveBeenCalledWith('Retry-After'); + }); + }); }); describe('IsolatedPromiseBuffer', () => {