Skip to content

Commit 469be3d

Browse files
committed
use dedupe approach
1 parent f5e75df commit 469be3d

File tree

1 file changed

+93
-63
lines changed

1 file changed

+93
-63
lines changed

packages/node-core/src/utils/outgoingFetchRequest.ts

Lines changed: 93 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { mergeBaggageHeaders } from './baggage';
1414
const SENTRY_TRACE_HEADER = 'sentry-trace';
1515
const SENTRY_BAGGAGE_HEADER = 'baggage';
1616

17-
const BAGGAGE_HEADER_REGEX_GLOBAL = /baggage: (.*)\r\n/g;
18-
const SENTRY_TRACE_HEADER_REGEX_GLOBAL = /sentry-trace: .*\r\n/g;
17+
// For baggage, we make sure to merge this into a possibly existing header
18+
const BAGGAGE_HEADER_REGEX = /baggage: (.*)\r\n/;
1919

2020
/**
2121
* Add trace propagation headers to an outgoing fetch/undici request.
@@ -45,6 +45,12 @@ export function addTracePropagationHeadersToFetchRequest(
4545

4646
const { 'sentry-trace': sentryTrace, baggage, traceparent } = addedHeaders;
4747

48+
// OTel's UndiciInstrumentation calls propagation.inject() which unconditionally
49+
// appends headers to the request. When the user also sets headers via getTraceData(),
50+
// this results in duplicate sentry-trace and baggage entries.
51+
// We clean these up before applying our own logic.
52+
_deduplicateHeaders(request);
53+
4854
// We do not want to overwrite existing headers here
4955
// If the core UndiciInstrumentation is registered, it will already have set the headers
5056
// We do not want to add any then
@@ -60,89 +66,113 @@ export function addTracePropagationHeadersToFetchRequest(
6066
requestHeaders.push('traceparent', traceparent);
6167
}
6268

63-
// Consolidate all duplicate baggage entries into one, then merge with our new baggage.
64-
// OTel's UndiciInstrumentation may append a second baggage header via propagation.inject(),
65-
// so we need to handle multiple entries — not just the first one.
66-
const baggagePositions: number[] = [];
67-
for (let i = 0; i < requestHeaders.length; i++) {
68-
if (requestHeaders[i] === SENTRY_BAGGAGE_HEADER) {
69-
baggagePositions.push(i);
70-
}
71-
}
72-
73-
if (baggage && !baggagePositions.length) {
69+
// For baggage, we make sure to merge this into a possibly existing header
70+
const existingBaggagePos = requestHeaders.findIndex(header => header === SENTRY_BAGGAGE_HEADER);
71+
if (baggage && existingBaggagePos === -1) {
7472
requestHeaders.push(SENTRY_BAGGAGE_HEADER, baggage);
7573
} else if (baggage) {
76-
// First, consolidate all existing baggage values into one
77-
let consolidatedBaggage = requestHeaders[baggagePositions[0]! + 1] as string;
78-
for (let i = baggagePositions.length - 1; i >= 1; i--) {
79-
const pos = baggagePositions[i]!;
80-
const val = requestHeaders[pos + 1] as string;
81-
consolidatedBaggage = mergeBaggageHeaders(consolidatedBaggage, val) || consolidatedBaggage;
82-
requestHeaders.splice(pos, 2);
83-
}
84-
85-
// Then merge with the new baggage we want to add
86-
const merged = mergeBaggageHeaders(consolidatedBaggage, baggage);
74+
// headers in format [key_0, value_0, key_1, value_1, ...], hence the +1 here
75+
const existingBaggage = requestHeaders[existingBaggagePos + 1];
76+
const merged = mergeBaggageHeaders(existingBaggage, baggage);
8777
if (merged) {
88-
requestHeaders[baggagePositions[0]! + 1] = merged;
89-
}
90-
}
91-
92-
// Also deduplicate sentry-trace headers — keep only the first occurrence.
93-
// OTel's UndiciInstrumentation may have appended a second one via propagation.inject().
94-
let firstSentryTraceFound = false;
95-
for (let i = requestHeaders.length - 2; i >= 0; i--) {
96-
if (requestHeaders[i] === SENTRY_TRACE_HEADER) {
97-
if (firstSentryTraceFound) {
98-
requestHeaders.splice(i, 2);
99-
}
100-
firstSentryTraceFound = true;
78+
requestHeaders[existingBaggagePos + 1] = merged;
10179
}
10280
}
10381
} else {
104-
const requestHeaders = request.headers;
10582
// We do not want to overwrite existing header here, if it was already set
106-
if (sentryTrace && !requestHeaders.includes(`${SENTRY_TRACE_HEADER}:`)) {
83+
if (sentryTrace && !request.headers.includes(`${SENTRY_TRACE_HEADER}:`)) {
10784
request.headers += `${SENTRY_TRACE_HEADER}: ${sentryTrace}\r\n`;
10885
}
10986

110-
if (traceparent && !requestHeaders.includes('traceparent:')) {
87+
if (traceparent && !request.headers.includes('traceparent:')) {
11188
request.headers += `traceparent: ${traceparent}\r\n`;
11289
}
11390

114-
// Consolidate all duplicate baggage entries into one, then merge with our new baggage.
115-
// OTel's UndiciInstrumentation may append a second baggage header via propagation.inject(),
116-
// so we need to handle multiple entries — not just the first one.
117-
const allBaggageMatches = request.headers.matchAll(BAGGAGE_HEADER_REGEX_GLOBAL);
118-
let consolidatedBaggage: string | undefined;
119-
for (const match of allBaggageMatches) {
120-
if (match[1]) {
121-
consolidatedBaggage = consolidatedBaggage
122-
? mergeBaggageHeaders(consolidatedBaggage, match[1]) || consolidatedBaggage
123-
: match[1];
91+
const existingBaggage = request.headers.match(BAGGAGE_HEADER_REGEX)?.[1];
92+
if (baggage && !existingBaggage) {
93+
request.headers += `${SENTRY_BAGGAGE_HEADER}: ${baggage}\r\n`;
94+
} else if (baggage) {
95+
const merged = mergeBaggageHeaders(existingBaggage, baggage);
96+
if (merged) {
97+
request.headers = request.headers.replace(BAGGAGE_HEADER_REGEX, `baggage: ${merged}\r\n`);
12498
}
12599
}
100+
}
101+
}
126102

127-
// Remove all existing baggage entries
128-
request.headers = request.headers.replace(BAGGAGE_HEADER_REGEX_GLOBAL, '');
103+
/**
104+
* Remove duplicate sentry-trace and baggage headers from the request.
105+
*
106+
* OTel's UndiciInstrumentation unconditionally appends headers via propagation.inject(),
107+
* which can create duplicates when the user has already set these headers (e.g. via getTraceData()).
108+
* For sentry-trace, we keep the first occurrence (user-set).
109+
* For baggage, we merge all occurrences into one to preserve both sentry and non-sentry entries.
110+
*/
111+
function _deduplicateHeaders(request: UndiciRequest): void {
112+
if (Array.isArray(request.headers)) {
113+
_deduplicateArrayHeaders(request.headers);
114+
} else if (typeof request.headers === 'string') {
115+
request.headers = _deduplicateStringHeaders(request.headers);
116+
}
117+
}
129118

130-
if (baggage && !consolidatedBaggage) {
131-
request.headers += `${SENTRY_BAGGAGE_HEADER}: ${baggage}\r\n`;
132-
} else if (baggage && consolidatedBaggage) {
133-
const merged = mergeBaggageHeaders(consolidatedBaggage, baggage);
119+
function _deduplicateArrayHeaders(headers: (string | string[])[]): void {
120+
_deduplicateArrayHeader(headers, SENTRY_TRACE_HEADER);
121+
_deduplicateArrayHeader(headers, SENTRY_BAGGAGE_HEADER);
122+
}
123+
124+
/**
125+
* For a given header name, if there are multiple entries in the [key, value, key, value, ...] array,
126+
* keep the first entry and remove the rest.
127+
* For baggage, values are merged to preserve all entries. For other headers, the first value wins.
128+
*/
129+
function _deduplicateArrayHeader(headers: (string | string[])[], name: string): void {
130+
let firstPos = -1;
131+
for (let i = 0; i < headers.length; i += 2) {
132+
if (headers[i] !== name) {
133+
continue;
134+
}
135+
136+
if (firstPos === -1) {
137+
firstPos = i;
138+
continue;
139+
}
140+
141+
// Duplicate found after firstPos. Merge into firstPos and remove.
142+
if (name === SENTRY_BAGGAGE_HEADER) {
143+
const merged = mergeBaggageHeaders(headers[firstPos + 1] as string, headers[i + 1] as string);
134144
if (merged) {
135-
request.headers += `${SENTRY_BAGGAGE_HEADER}: ${merged}\r\n`;
145+
headers[firstPos + 1] = merged;
136146
}
137147
}
148+
headers.splice(i, 2);
149+
i -= 2;
150+
}
151+
}
152+
153+
function _deduplicateStringHeaders(input: string): string {
154+
// Deduplicate sentry-trace — keep only the first occurrence
155+
let sentryTraceCount = 0;
156+
let result = input.replace(/sentry-trace: .*\r\n/g, match => {
157+
return ++sentryTraceCount === 1 ? match : '';
158+
});
159+
160+
// Deduplicate baggage — merge all occurrences into one
161+
let mergedBaggage: string | undefined;
162+
result = result.replace(/baggage: (.*)\r\n/g, (_match, value: string) => {
163+
if (!mergedBaggage) {
164+
mergedBaggage = value;
165+
} else {
166+
mergedBaggage = mergeBaggageHeaders(mergedBaggage, value) || mergedBaggage;
167+
}
168+
return '';
169+
});
138170

139-
// Deduplicate sentry-trace headers — keep only the first occurrence.
140-
let sentryTraceCount = 0;
141-
request.headers = request.headers.replace(SENTRY_TRACE_HEADER_REGEX_GLOBAL, match => {
142-
sentryTraceCount++;
143-
return sentryTraceCount === 1 ? match : '';
144-
});
171+
if (mergedBaggage) {
172+
result += `${SENTRY_BAGGAGE_HEADER}: ${mergedBaggage}\r\n`;
145173
}
174+
175+
return result;
146176
}
147177

148178
/** Add a breadcrumb for an outgoing fetch/undici request. */

0 commit comments

Comments
 (0)