Skip to content

Commit 526486f

Browse files
authored
fix(undici): avoid possible duplicate 'traceparent' header on instrumented HTTP requests (#3965)
... because Elasticsearch borks on these requests. This case should only be possible in a weird case like having two APM agents active. Fixes: #3964
1 parent 335ab65 commit 526486f

File tree

3 files changed

+75
-10
lines changed

3 files changed

+75
-10
lines changed

CHANGELOG.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ See the <<upgrade-to-v4>> guide.
5050
* Fix undici instrumentation to cope with a bug in [email protected] where
5151
`request.addHeader()` was accidentally removed. (It was re-added in
5252
53+
* Update undici instrumentation to avoid possibly adding a *second*
54+
'traceparent' header to outgoing HTTP requests, because this can break
55+
Elasticsearch requests. ({issues}3964[#3964])
5356
5457
[float]
5558
===== Chores

lib/instrumentation/modules/undici.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ try {
4444

4545
const semver = require('semver');
4646

47+
// Search an undici@5 request.headers string for a 'traceparent' header.
48+
const headersStrHasTraceparentRe = /^traceparent:/im;
49+
4750
let isInstrumented = false;
4851
let spanFromReq = null;
4952
let chans = null;
@@ -130,17 +133,37 @@ function instrumentUndici(agent) {
130133
const propSpan =
131134
span || parentRunContext.currSpan() || parentRunContext.currTransaction();
132135
if (propSpan) {
133-
propSpan.propagateTraceContextHeaders(
134-
request,
135-
function (req, name, value) {
136-
if (typeof request.addHeader === 'function') {
137-
req.addHeader(name, value);
138-
} else if (Array.isArray(request.headers)) {
139-
// [email protected] accidentally, briefly removed `request.addHeader()`.
140-
req.headers.push(name, value);
136+
// Guard against adding a duplicate 'traceparent' header, because that
137+
// breaks ES. https://github.com/elastic/apm-agent-nodejs/issues/3964
138+
// Dev Note: This cheats a little and assumes the header names to add
139+
// will include 'traceparent'.
140+
let alreadyHasTp = false;
141+
if (Array.isArray(request.headers)) {
142+
// undici@6
143+
for (let i = 0; i < request.headers.length; i += 2) {
144+
if (request.headers[i].toLowerCase() === 'traceparent') {
145+
alreadyHasTp = true;
146+
break;
141147
}
142-
},
143-
);
148+
}
149+
} else if (typeof request.headers === 'string') {
150+
// undici@5
151+
alreadyHasTp = headersStrHasTraceparentRe.test(request.headers);
152+
}
153+
154+
if (!alreadyHasTp) {
155+
propSpan.propagateTraceContextHeaders(
156+
request,
157+
function (req, name, value) {
158+
if (typeof request.addHeader === 'function') {
159+
req.addHeader(name, value);
160+
} else if (Array.isArray(request.headers)) {
161+
// [email protected] accidentally, briefly removed `request.addHeader()`.
162+
req.headers.push(name, value);
163+
}
164+
},
165+
);
166+
}
144167
}
145168

146169
if (span) {

test/instrumentation/modules/undici/undici.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,45 @@ if (global.AbortController) {
274274
});
275275
}
276276

277+
test('undici no duplicate headers', async (t) => {
278+
apm._apmClient.clear();
279+
const aTrans = apm.startTransaction('aTransName');
280+
281+
const url = origin + '/ping';
282+
const { statusCode, body } = await undici.request(url, {
283+
headers: {
284+
traceparent: 'this-value-is-not-from-elastic-apm-node',
285+
},
286+
});
287+
t.equal(statusCode, 200, 'statusCode');
288+
await consumeResponseBody(body);
289+
290+
aTrans.end();
291+
t.error(await promisyApmFlush(), 'no apm.flush() error');
292+
293+
t.equal(apm._apmClient.spans.length, 1);
294+
const span = apm._apmClient.spans[0];
295+
assertUndiciSpan(t, span, url);
296+
297+
// If there is already a 'traceparent' header in the way, the APM agent
298+
// should at least *not* result in duplicate headers, even if that means
299+
// breaking distributed tracing.
300+
let numTraceparentHeaders = 0;
301+
let numTracestateHeaders = 0;
302+
for (let i = 0; i < lastServerReq.rawHeaders.length; i += 2) {
303+
const k = lastServerReq.rawHeaders[i].toLowerCase();
304+
if (k === 'traceparent') {
305+
numTraceparentHeaders++;
306+
} else if (k === 'tracestate') {
307+
numTracestateHeaders++;
308+
}
309+
}
310+
t.equal(numTraceparentHeaders, 1, 'just one traceparent header');
311+
t.equal(numTracestateHeaders, 0, 'tracestate header was skipped');
312+
313+
t.end();
314+
});
315+
277316
test('teardown', (t) => {
278317
undici.getGlobalDispatcher().close(); // Close kept-alive sockets.
279318
server.close();

0 commit comments

Comments
 (0)