From 4d64142e136b6d9f1177c6005c1df5670c39e976 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 21 Apr 2026 15:42:10 -0400 Subject: [PATCH 1/3] fix(node): Guard against null `httpVersion` in outgoing request span attributes On Node 22+ with Next.js 15, the bundled `@mswjs/interceptors` emits a synthetic `response` event on request timeout/abort whose `IncomingMessage` has `httpVersion === null`. `_getOutgoingRequestEndedSpanData` called `httpVersion.toUpperCase()` unguarded, which crashed the process with an unhandled rejection. The sibling server-side code in `httpServerSpansIntegration` already optional-chains this attribute; this brings the client path in line. Fixes #20415 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/integrations/http/SentryHttpInstrumentation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index b25d32138aa9..a8072a9413cd 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -459,7 +459,10 @@ function _getOutgoingRequestSpanData(request: http.ClientRequest): [string, Span function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes { const { statusCode, statusMessage, httpVersion, socket } = response; - const transport = httpVersion.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp'; + // httpVersion can be undefined in some cases and we seem to hav encountered this before: + // https://github.com/getsentry/sentry-javascript/blob/ec8c8c64cde6001123db0199a8ca017b8863eac8/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts#L158 + // see: #20415 + const transport = httpVersion?.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp'; const additionalAttributes: SpanAttributes = { [ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode, From 1dc6e72b2dfff8499d7e6de6570a50957d49e946 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 21 Apr 2026 21:49:16 +0200 Subject: [PATCH 2/3] Update packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/integrations/http/SentryHttpInstrumentation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index a8072a9413cd..def6a2556da0 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -459,7 +459,7 @@ function _getOutgoingRequestSpanData(request: http.ClientRequest): [string, Span function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes { const { statusCode, statusMessage, httpVersion, socket } = response; - // httpVersion can be undefined in some cases and we seem to hav encountered this before: + // httpVersion can be undefined in some cases and we seem to have encountered this before: // https://github.com/getsentry/sentry-javascript/blob/ec8c8c64cde6001123db0199a8ca017b8863eac8/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts#L158 // see: #20415 const transport = httpVersion?.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp'; From bf64097d72991ca9b34331222b9b06a40ed6dd5d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 22 Apr 2026 08:49:55 -0400 Subject: [PATCH 3/3] test(node-core): Add unit tests for `_getOutgoingRequestEndedSpanData` null httpVersion guard Co-Authored-By: Claude Opus 4.7 (1M context) --- .../http/SentryHttpInstrumentation.ts | 5 +- .../SentryHttpInstrumentation.test.ts | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index def6a2556da0..60cf7bbae9aa 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -456,7 +456,10 @@ function _getOutgoingRequestSpanData(request: http.ClientRequest): [string, Span ]; } -function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes { +/** + * Exported for testing purposes. + */ +export function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes { const { statusCode, statusMessage, httpVersion, socket } = response; // httpVersion can be undefined in some cases and we seem to have encountered this before: diff --git a/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts b/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts new file mode 100644 index 000000000000..182abaa3663f --- /dev/null +++ b/packages/node-core/test/integrations/SentryHttpInstrumentation.test.ts @@ -0,0 +1,48 @@ +import type * as http from 'node:http'; +import { describe, expect, it } from 'vitest'; +import { _getOutgoingRequestEndedSpanData } from '../../src/integrations/http/SentryHttpInstrumentation'; + +function createResponse(overrides: Partial): http.IncomingMessage { + return { + statusCode: 200, + statusMessage: 'OK', + httpVersion: '1.1', + headers: {}, + socket: undefined, + ...overrides, + } as unknown as http.IncomingMessage; +} + +describe('_getOutgoingRequestEndedSpanData', () => { + it('sets ip_tcp transport for HTTP/1.1', () => { + const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: '1.1' })); + + expect(attributes['network.transport']).toBe('ip_tcp'); + expect(attributes['net.transport']).toBe('ip_tcp'); + expect(attributes['network.protocol.version']).toBe('1.1'); + expect(attributes['http.flavor']).toBe('1.1'); + }); + + it('sets ip_udp transport for QUIC', () => { + const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: 'QUIC' })); + + expect(attributes['network.transport']).toBe('ip_udp'); + expect(attributes['net.transport']).toBe('ip_udp'); + }); + + it('does not throw when httpVersion is null', () => { + expect(() => + _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: null as unknown as string })), + ).not.toThrow(); + + const attributes = _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: null as unknown as string })); + expect(attributes['network.transport']).toBe('ip_tcp'); + expect(attributes['net.transport']).toBe('ip_tcp'); + }); + + it('does not throw when httpVersion is undefined', () => { + expect(() => + _getOutgoingRequestEndedSpanData(createResponse({ httpVersion: undefined as unknown as string })), + ).not.toThrow(); + }); +});