Skip to content

Commit 6007a26

Browse files
Enforced W3 specification for 'baggage' header
1 parent 02a539a commit 6007a26

File tree

10 files changed

+362
-63
lines changed

10 files changed

+362
-63
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
import { isDatadogCustomHeader } from '../headers';
7+
8+
describe('headers', () => {
9+
describe('isDatadogCustomHeader', () => {
10+
it('returns false for non-custom headers', () => {
11+
expect(isDatadogCustomHeader('non-custom-header')).toBeFalsy();
12+
});
13+
});
14+
});

packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracing.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ export type DdRumResourceTracingAttributes =
2626
rulePsr: number;
2727
propagatorTypes: PropagatorType[];
2828
rumSessionId?: string;
29-
baggageHeaders?: Set<string>;
3029
}
3130
| {
3231
tracingStrategy: 'DISCARD';
3332
traceId?: void;
3433
spanId?: void;
3534
samplingPriorityHeader: '0';
36-
baggageHeaders?: Set<string>;
3735
};
3836

3937
const DISCARDED_TRACE_ATTRIBUTES: DdRumResourceTracingAttributes = {

packages/core/src/rum/instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -137,27 +137,15 @@ export const getTracingHeadersFromAttributes = (
137137
);
138138
}
139139
}
140-
if (tracingAttributes.rumSessionId) {
141-
if (!tracingAttributes.baggageHeaders) {
142-
tracingAttributes.baggageHeaders = new Set<string>();
143-
}
144-
145-
tracingAttributes.baggageHeaders?.add(
146-
`${DD_RUM_SESSION_ID_TAG}=${tracingAttributes.rumSessionId}`
147-
);
148-
}
149-
150-
const baggageHeader = tracingAttributes.baggageHeaders
151-
? Array.from(tracingAttributes.baggageHeaders).join(', ')
152-
: null;
153-
if (baggageHeader) {
154-
headers.push({
155-
header: BAGGAGE_HEADER_KEY,
156-
value: baggageHeader
157-
});
158-
}
159140
});
160141

142+
if (tracingAttributes.rumSessionId) {
143+
headers.push({
144+
header: BAGGAGE_HEADER_KEY,
145+
value: `${DD_RUM_SESSION_ID_TAG}=${tracingAttributes.rumSessionId}`
146+
});
147+
}
148+
161149
return headers;
162150
};
163151

packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
* Copyright 2016-Present Datadog, Inc.
55
*/
66

7+
import { isDatadogCustomHeader } from '../../headers';
78
import {
89
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
910
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
10-
DATADOG_GRAPH_QL_VARIABLES_HEADER,
11-
isDatadogCustomHeader
11+
DATADOG_GRAPH_QL_VARIABLES_HEADER
1212
} from '../graphqlHeaders';
1313

1414
describe('GraphQL custom headers', () => {
@@ -19,10 +19,4 @@ describe('GraphQL custom headers', () => {
1919
])('%s matches the custom header pattern', header => {
2020
expect(isDatadogCustomHeader(header)).toBeTruthy();
2121
});
22-
23-
describe('isDatadogCustomHeader', () => {
24-
it('returns false for non-custom headers', () => {
25-
expect(isDatadogCustomHeader('non-custom-header')).toBeFalsy();
26-
});
27-
});
2822
});
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1+
import { DATADOG_CUSTOM_HEADER_PREFIX } from '../headers';
2+
13
/*
24
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
35
* This product includes software developed at Datadog (https://www.datadoghq.com/).
46
* Copyright 2016-Present Datadog, Inc.
57
*/
6-
7-
const DATADOG_CUSTOM_HEADER_PREFIX = '_dd-custom-header';
8-
98
export const DATADOG_GRAPH_QL_OPERATION_NAME_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-name`;
109
export const DATADOG_GRAPH_QL_VARIABLES_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-variables`;
1110
export const DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-type`;
12-
13-
export const isDatadogCustomHeader = (header: string) => {
14-
return header.match(new RegExp(`^${DATADOG_CUSTOM_HEADER_PREFIX}`));
15-
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
export const DATADOG_CUSTOM_HEADER_PREFIX = '_dd-custom-header';
8+
export const DATADOG_BAGGAGE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-baggage`;
9+
10+
export const isDatadogCustomHeader = (header: string) => {
11+
return header.match(new RegExp(`^${DATADOG_CUSTOM_HEADER_PREFIX}`));
12+
};

packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import { getTracingAttributes } from '../../distributedTracing/distributedTracin
1515
import {
1616
DATADOG_GRAPH_QL_OPERATION_NAME_HEADER,
1717
DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER,
18-
DATADOG_GRAPH_QL_VARIABLES_HEADER,
19-
isDatadogCustomHeader
18+
DATADOG_GRAPH_QL_VARIABLES_HEADER
2019
} from '../../graphql/graphqlHeaders';
20+
import { DATADOG_BAGGAGE_HEADER, isDatadogCustomHeader } from '../../headers';
2121
import type { RequestProxyOptions } from '../interfaces/RequestProxy';
2222
import { RequestProxy } from '../interfaces/RequestProxy';
2323

2424
import type { ResourceReporter } from './DatadogRumResource/ResourceReporter';
2525
import { URLHostParser } from './URLHostParser';
26+
import { formatBaggageHeader } from './baggageHeaderUtils';
2627
import { calculateResponseSize } from './responseSize';
2728

2829
const RESPONSE_START_LABEL = 'response_start';
@@ -42,6 +43,7 @@ interface DdRumXhrContext {
4243
reported: boolean;
4344
timer: Timer;
4445
tracingAttributes: DdRumResourceTracingAttributes;
46+
baggageHeaderEntries: Set<string>;
4547
}
4648

4749
interface XHRProxyProviders {
@@ -114,7 +116,8 @@ const proxyOpen = (
114116
firstPartyHostsRegexMap,
115117
tracingSamplingRate,
116118
rumSessionId: getCachedSessionId()
117-
})
119+
}),
120+
baggageHeaderEntries: new Set<string>()
118121
};
119122
// eslint-disable-next-line prefer-rest-params
120123
return originalXhrOpen.apply(this, arguments as any);
@@ -130,12 +133,22 @@ const proxySend = (providers: XHRProxyProviders): void => {
130133
// keep track of start time
131134
this._datadog_xhr.timer.start();
132135

136+
// Tracing Headers
133137
const tracingHeaders = getTracingHeadersFromAttributes(
134138
this._datadog_xhr.tracingAttributes
135139
);
140+
136141
tracingHeaders.forEach(({ header, value }) => {
137142
this.setRequestHeader(header, value);
138143
});
144+
145+
// Join all baggage header entries
146+
const baggageHeader = formatBaggageHeader(
147+
this._datadog_xhr.baggageHeaderEntries
148+
);
149+
if (baggageHeader) {
150+
this.setRequestHeader(DATADOG_BAGGAGE_HEADER, baggageHeader);
151+
}
139152
}
140153

141154
proxyOnReadyStateChange(this, providers);
@@ -214,30 +227,37 @@ const proxySetRequestHeader = (providers: XHRProxyProviders): void => {
214227
header: string,
215228
value: string
216229
) {
217-
if (isDatadogCustomHeader(header)) {
218-
if (header === DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) {
219-
this._datadog_xhr.graphql.operationName = value;
220-
return;
230+
const key = header.toLowerCase();
231+
if (isDatadogCustomHeader(key)) {
232+
switch (key) {
233+
case DATADOG_GRAPH_QL_OPERATION_NAME_HEADER:
234+
this._datadog_xhr.graphql.operationName = value;
235+
break;
236+
case DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER:
237+
this._datadog_xhr.graphql.operationType = value;
238+
break;
239+
case DATADOG_GRAPH_QL_VARIABLES_HEADER:
240+
this._datadog_xhr.graphql.variables = value;
241+
break;
242+
case DATADOG_BAGGAGE_HEADER:
243+
// Apply Baggage Header only if pre-processed by Datadog
244+
return originalXhrSetRequestHeader.apply(this, [
245+
BAGGAGE_HEADER_KEY,
246+
value
247+
]);
248+
default:
249+
return originalXhrSetRequestHeader.apply(
250+
this,
251+
// eslint-disable-next-line prefer-rest-params
252+
arguments as any
253+
);
221254
}
222-
if (header === DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) {
223-
this._datadog_xhr.graphql.operationType = value;
224-
return;
225-
}
226-
if (header === DATADOG_GRAPH_QL_VARIABLES_HEADER) {
227-
this._datadog_xhr.graphql.variables = value;
228-
return;
229-
}
230-
}
231-
232-
if (header.toLowerCase() === BAGGAGE_HEADER_KEY) {
233-
if (!this._datadog_xhr.tracingAttributes.baggageHeaders) {
234-
this._datadog_xhr.tracingAttributes.baggageHeaders = new Set<string>();
235-
}
236-
237-
this._datadog_xhr.tracingAttributes.baggageHeaders?.add(value);
255+
} else if (key === BAGGAGE_HEADER_KEY) {
256+
// Intercept User Baggage Header entries to apply them later
257+
this._datadog_xhr.baggageHeaderEntries?.add(value);
258+
} else {
259+
// eslint-disable-next-line prefer-rest-params
260+
return originalXhrSetRequestHeader.apply(this, arguments as any);
238261
}
239-
240-
// eslint-disable-next-line prefer-rest-params
241-
return originalXhrSetRequestHeader.apply(this, arguments as any);
242262
};
243263
};

packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ describe('XHRProxy', () => {
882882
);
883883

884884
const values = xhr.requestHeaders[BAGGAGE_HEADER_KEY].split(
885-
', '
885+
','
886886
).sort();
887887

888888
expect(values[0]).toBe('existing.key=existing-value');
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { InternalLog } from '../../../../../../InternalLog';
2+
import { SdkVerbosity } from '../../../../../../SdkVerbosity';
3+
import { formatBaggageHeader } from '../baggageHeaderUtils';
4+
5+
describe('formatBaggageHeader', () => {
6+
let logSpy: jest.SpyInstance;
7+
8+
beforeEach(() => {
9+
logSpy = jest.spyOn(InternalLog, 'log').mockImplementation(() => {});
10+
});
11+
12+
afterEach(() => {
13+
logSpy.mockRestore();
14+
});
15+
16+
it('should format simple key=value entries correctly', () => {
17+
const entries = new Set(['userId=alice', 'isProduction=false']);
18+
const result = formatBaggageHeader(entries);
19+
expect(result).toBe('userId=alice,isProduction=false');
20+
expect(logSpy).not.toHaveBeenCalled();
21+
});
22+
23+
it('should percent-encode spaces and non-ASCII characters in values', () => {
24+
const entries = new Set(['user=Amélie', 'region=us east']);
25+
const result = formatBaggageHeader(entries);
26+
expect(result).toBe('user=Am%C3%A9lie,region=us%20east');
27+
});
28+
29+
it('should support properties with and without values', () => {
30+
const entries = new Set(['traceId=abc123;sampled=true;debug']);
31+
const result = formatBaggageHeader(entries);
32+
expect(result).toBe('traceId=abc123;sampled=true;debug');
33+
});
34+
35+
it('should trim whitespace around keys, values, and properties', () => {
36+
const entries = new Set([' foo = bar ; p1 = one ; p2 ']);
37+
const result = formatBaggageHeader(entries);
38+
expect(result).toBe('foo=bar;p1=one;p2');
39+
});
40+
41+
it('should skip invalid entries without crashing', () => {
42+
const entries = new Set(['valid=ok', 'invalidEntry']);
43+
const result = formatBaggageHeader(entries);
44+
expect(result).toBe('valid=ok');
45+
expect(logSpy).toHaveBeenCalledWith(
46+
expect.stringContaining('Dropped invalid baggage header entry'),
47+
SdkVerbosity.WARN
48+
);
49+
});
50+
51+
it('should skip entries with invalid key (non-token)', () => {
52+
const entries = new Set(['in valid=value', 'user=ok']);
53+
const result = formatBaggageHeader(entries);
54+
expect(result).toBe('user=ok');
55+
expect(logSpy).toHaveBeenCalledWith(
56+
expect.stringContaining('key not compliant'),
57+
SdkVerbosity.WARN
58+
);
59+
});
60+
61+
it('should skip invalid properties (bad property key)', () => {
62+
const entries = new Set(['user=ok;invalid key=value;good=yes']);
63+
const result = formatBaggageHeader(entries);
64+
expect(result).toBe('user=ok;good=yes');
65+
expect(logSpy).toHaveBeenCalledWith(
66+
expect.stringContaining('property key not compliant'),
67+
SdkVerbosity.WARN
68+
);
69+
});
70+
71+
it('should log warning when too many members (>64)', () => {
72+
const entries = new Set<string>();
73+
for (let i = 0; i < 70; i++) {
74+
entries.add(`k${i}=v${i}`);
75+
}
76+
const result = formatBaggageHeader(entries);
77+
expect(result?.startsWith('k0=v0')).toBe(true);
78+
expect(logSpy).toHaveBeenCalledWith(
79+
expect.stringContaining('Too many baggage members'),
80+
SdkVerbosity.WARN
81+
);
82+
});
83+
84+
it('should log warning when header exceeds byte limit', () => {
85+
const bigValue = 'x'.repeat(9000);
86+
const entries = new Set([`large=${bigValue}`]);
87+
const result = formatBaggageHeader(entries);
88+
expect(result).toContain('large=');
89+
expect(logSpy).toHaveBeenCalledWith(
90+
expect.stringContaining('Baggage header too large'),
91+
SdkVerbosity.WARN
92+
);
93+
});
94+
95+
it('should return null if all entries are invalid', () => {
96+
const entries = new Set(['badEntry', 'stillBad']);
97+
const result = formatBaggageHeader(entries);
98+
expect(result).toBeNull();
99+
});
100+
101+
it('should preserve insertion order', () => {
102+
const entries = new Set(['first=1', 'second=2', 'third=3']);
103+
const result = formatBaggageHeader(entries);
104+
expect(result).toBe('first=1,second=2,third=3');
105+
});
106+
});

0 commit comments

Comments
 (0)