Repository navigation
node: Guard user callbacks in the node:http server instrumentation #25590
Description
PR #25581 adds onSpanEnd to Node's httpIntegration. Bugbot found that a hook that throws could skip span.end() and crash the process, so that PR now calls onSpanEnd through safeCallback. The same problem exists in two other places that the PR did not touch. Both are older than the PR.
The SDK rule (.cursor/BUGBOT.md): an exception in a user callback must never crash the host app. The SDK swallows it and logs an SDK error.
The web Request handlers (Deno.serve, Bun.serve, Cloudflare) already guard every user callback: onSpanCreated, onSpanEnd, ignoreIncomingRequests and ignoreRequestBody. Only the node:httpcode paths lack the guard.
Issue 1: Node onSpanCreated is not guarded
packages/node/src/integrations/http/httpServerSpansIntegration.ts:196
onSpanCreated?.(span, request, response);
return withActiveSpan(span, () => {
bindScopeToEmitter(request);
bindScopeToEmitter(response);
// ... registers the `close` and `error` listeners that end the span
return next();
});
If onSpanCreated throws:
- The span has started, but the listeners that end it are never registered, so it never ends and is never sent.
next()never runs, so the server's'request'event does not reach the user's handler. The error propagates out of the patchedserver.emit('request'), inside Node's HTTP parser. The request fails, and the error can surface as an uncaught exception.
Affected: @sentry/node httpIntegration and httpServerSpansIntegration, and every SDK that uses them (Bun, Next.js, Remix, SvelteKit, Astro, Nuxt, and the serverless SDKs).
Issue 2: core node:http server code calls both hooks unguarded
packages/core/src/integrations/http/server-subscription.ts
span => {
onSpanCreated?.(span, request, response); // line 357
// ...
function endSpan(status: SpanStatus): void {
// ...
span.setStatus(status);
onSpanEnd?.(span, request, response); // line 375
span.end();
}
response.once('close', () => { endSpan(...); });
response.once(errorMonitor, () => { endSpan(...); });
next();
},
If onSpanCreated throws, the result is the same as issue 1: the span never ends, next() never runs, and the request fails.
If onSpanEnd throws, span.end() is skipped, and the error comes from a close or error event listener, where it becomes an uncaught exception. This is the problem fixed for Node in #25581.
Affected: Deno denoHttpIntegration and Bun bunHttpServerIntegration, which pass both hooks to getHttpServerSubscriptions.
Same pattern, other callbacks
These node:http callbacks are also called without a guard. They should get the same fix, with these fallbacks:
| Callback | Location | Fallback if it throws |
|---|---|---|
ignoreIncomingRequests |
packages/node/src/integrations/http/httpServerSpansIntegration.ts:336 |
false(create the span) |
ignoreIncomingRequests |
packages/core/src/integrations/http/server-subscription.ts:422 |
false(create the span) |
ignoreRequestBody |
packages/core/src/integrations/http/server-subscription.ts:150 |
true(skip the body, so it cannot leak data the filter was meant to hide) |
These fallbacks match the web Request handler wrapper (packages/server-utils/src/utils/wrapServerRequestHandler.ts).
Proposed fix
Call each callback through safeCallback from @sentry/core, with a DEBUG_BUILD-gated message, as #25581 does for Node's onSpanEnd:
if (onSpanCreated) {
safeCallback(
DEBUG_BUILD ? 'Error in `onSpanCreated`:' : '',
() => onSpanCreated(span, request, response),
() => undefined,
);
}
safeCallback logs the error in debug builds and returns the fallback. It does not capture the error as a Sentry event, which the SDK rules forbid for errors in user callbacks.
Tests
- Node: integration tests in
dev-packages/node-integration-tests/suites/tracing/httpIntegration, following theonSpanEnd that throwstest from feat(node): AddonSpanEndtohttpIntegration#25581. ForonSpanCreatedandignoreIncomingRequests, check that the request still gets its response, that the span (if any) is sent, and that the server handles a second request. - Core: unit tests in
packages/core/test/lib/integrations/http/server-subscription.test.ts, one for each callback, checking that the request reaches its handler, the span ends, and nothing throws. - Deno: a test in
packages/deno/test/deno-http.test.tsfor a throwingonSpanEnd, since Deno runs the core code on a realnode:httpserver.
Done when
- No user callback in the
node:httpserver instrumentation (Node or core) is called without a guard. - A callback that throws never fails the request, never leaves a span open, and never causes an uncaught exception.
Activity
linear-code commentedon Oct 21, 2026
JS-3976