Skip to content

node: Guard user callbacks in the node:http server instrumentation #25590

Open

Description

@isaacs

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 patched server.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 the onSpanEnd that throws test from feat(node): Add onSpanEnd to httpIntegration #25581. For onSpanCreated and ignoreIncomingRequests, 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.ts for a throwing onSpanEnd, since Deno runs the core code on a real node:http server.

Done when

  • No user callback in the node:http server 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

  1. self-assigned this
    on Oct 21, 2026
  2. added
    javascriptPull requests that update javascript code
    on Oct 21, 2026
  3. linear-code commented on Oct 21, 2026

    @linear-code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

javascriptPull requests that update javascript code

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions