From 2625f8b260ce1723a851008ae5d91f8d73022cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusufhan=20Sa=C3=A7ak?= Date: Wed, 2 Sep 2026 22:00:50 +0300 Subject: [PATCH] fix(deno): Don't throw in Deno.serve wrapper when no client is bound The Deno.serve patch is installed by Client.init(), which a directly constructed client also runs without ever calling setCurrentClient, so the patch can be live with no client bound and every incoming request threw before reaching the user's handler. Pass through to the handler with a debug warning instead; the Cloudflare wrapper likewise never blocks a request when the client is missing. Fixes #23894 --- .../deno/src/wrap-deno-request-handler.ts | 8 ++- .../deno/test/deno-serve-no-client.test.ts | 67 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/deno/test/deno-serve-no-client.test.ts diff --git a/packages/deno/src/wrap-deno-request-handler.ts b/packages/deno/src/wrap-deno-request-handler.ts index bcfefe4cd868..1d53cca0db27 100644 --- a/packages/deno/src/wrap-deno-request-handler.ts +++ b/packages/deno/src/wrap-deno-request-handler.ts @@ -11,6 +11,7 @@ import { captureBodyFromWinterCGRequest, captureException, continueTrace, + debug, getClient, getHttpSpanDetailsFromUrlObject, hasSpanStreamingEnabled, @@ -48,7 +49,12 @@ export const wrapDenoRequestHandler = ( const client = getClient(); if (!client) { - throw new Error('could not get Deno client. Did you run Sentry.init?'); + // `denoServeIntegration` patches `Deno.serve` from `Client.init()`, which a + // directly-constructed client also runs — that path never calls + // `setCurrentClient`, so the patch can be live with no client bound. Keep + // requests flowing to the user's handler, uninstrumented. + debug.warn('Cannot instrument Deno.serve request. No client defined.'); + return handler(); } isolationScope.setClient(client); diff --git a/packages/deno/test/deno-serve-no-client.test.ts b/packages/deno/test/deno-serve-no-client.test.ts new file mode 100644 index 000000000000..08dc5a67eb7f --- /dev/null +++ b/packages/deno/test/deno-serve-no-client.test.ts @@ -0,0 +1,67 @@ +// + +/** + * Lives in its own file because it wipes the global carrier; Deno gives each test + * file a fresh module graph, so the wipe stays contained here. + */ + +import { getMainCarrier } from '@sentry/core'; +import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; +import { assertNotEquals } from 'https://deno.land/std@0.212.0/assert/assert_not_equals.ts'; +import { init } from '../build/esm/index.js'; + +function resetGlobals(): void { + getMainCarrier().__SENTRY__ = undefined; +} + +// Captured before any init() so the patch assertion below compares against the +// genuinely unpatched function; the patch installs once per module graph. +const unpatchedServe = Deno.serve; + +function initPatchedServeWithoutClient(): void { + // Install the Deno.serve patch, then unbind the client. The patch is installed by + // `Client.init()`, which a directly-constructed client also runs without ever + // calling `setCurrentClient` — so a live patch with no bound client is a real state. + resetGlobals(); + init({ dsn: 'https://username@domain/123' }); + assertNotEquals(Deno.serve, unpatchedServe, 'Deno.serve was not patched; test would pass vacuously'); + resetGlobals(); +} + +Deno.test('Deno.serve keeps serving when no client is bound', async () => { + initPatchedServeWithoutClient(); + + const abortController = new AbortController(); + let onListen: ((_: unknown) => void) | undefined = undefined; + const p = new Promise(resolve => (onListen = resolve)); + const server = Deno.serve({ port: 0, signal: abortController.signal, onListen }, () => { + return new Response('Hello World'); + }); + await p; + + const response = await fetch(`http://localhost:${server.addr.port}/test`); + assertEquals(response.status, 200); + assertEquals(await response.text(), 'Hello World'); + + abortController.abort(); + await server.finished; +}); + +Deno.test('Deno.serve propagates handler errors as 500 when no client is bound', async () => { + initPatchedServeWithoutClient(); + + const abortController = new AbortController(); + let onListen: ((_: unknown) => void) | undefined = undefined; + const p = new Promise(resolve => (onListen = resolve)); + const server = Deno.serve({ port: 0, signal: abortController.signal, onListen }, () => { + throw new Error('handler blew up'); + }); + await p; + + const response = await fetch(`http://localhost:${server.addr.port}/boom`); + assertEquals(response.status, 500); + await response.body?.cancel(); + + abortController.abort(); + await server.finished; +});