@@ -14,8 +14,8 @@ import { mergeBaggageHeaders } from './baggage';
1414const SENTRY_TRACE_HEADER = 'sentry-trace' ;
1515const SENTRY_BAGGAGE_HEADER = 'baggage' ;
1616
17- const BAGGAGE_HEADER_REGEX_GLOBAL = / b a g g a g e : ( . * ) \r \n / g ;
18- const SENTRY_TRACE_HEADER_REGEX_GLOBAL = / s e n t r y - t r a c e : . * \r \n / g ;
17+ // For baggage, we make sure to merge this into a possibly existing header
18+ const BAGGAGE_HEADER_REGEX = / b a g g a g e : ( . * ) \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 ( / s e n t r y - t r a c e : .* \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 ( / b a g g a g e : ( .* ) \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