Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
fetchButton.addEventListener('click', () => {
// W3C spec example: property values can contain = signs
// See: https://www.w3.org/TR/baggage/#example
fetch('http://sentry-test-site.example/fetch-test', {
headers: {
baggage: 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue',
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fetch Baggage Property Values Test</title>
</head>
<body>
<button id="fetchButton">Make Fetch Request</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { expect } from '@playwright/test';
import { TRACEPARENT_REGEXP } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest(
'preserves baggage property values with equal signs in fetch requests',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const requestPromise = page.waitForRequest('http://sentry-test-site.example/fetch-test');

await page.goto(url);
await page.click('#fetchButton');

const request = await requestPromise;

const requestHeaders = request.headers();

expect(requestHeaders).toMatchObject({
'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP),
});

const baggageHeader = requestHeaders.baggage;
expect(baggageHeader).toBeDefined();

const baggageItems = baggageHeader.split(',').map(item => decodeURIComponent(item.trim()));

// Verify property values with = signs are preserved
expect(baggageItems).toContainEqual(expect.stringContaining('key1=value1;property1;property2'));
expect(baggageItems).toContainEqual(expect.stringContaining('key2=value2'));
expect(baggageItems).toContainEqual(expect.stringContaining('key3=value3; propertyKey=propertyValue'));

// Verify Sentry baggage is also present
expect(baggageHeader).toMatch(/sentry-/);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const xhr = new XMLHttpRequest();

xhr.open('GET', 'http://sentry-test-site.example/1');
// W3C spec example: property values can contain = signs
// See: https://www.w3.org/TR/baggage/#example
xhr.setRequestHeader('baggage', 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue');

xhr.send();
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect } from '@playwright/test';
import { TRACEPARENT_REGEXP } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest('preserves baggage property values with equal signs in XHR requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const requestPromise = page.waitForRequest('http://sentry-test-site.example/1');

await page.goto(url);

const request = await requestPromise;

const requestHeaders = request.headers();

expect(requestHeaders).toMatchObject({
'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP),
});

const baggageHeader = requestHeaders.baggage;
expect(baggageHeader).toBeDefined();
const baggageItems = baggageHeader.split(',').map(item => decodeURIComponent(item.trim()));

// Verify property values with = signs are preserved
expect(baggageItems).toContainEqual(expect.stringContaining('key1=value1;property1;property2'));
expect(baggageItems).toContainEqual(expect.stringContaining('key2=value2'));
expect(baggageItems).toContainEqual(expect.stringContaining('key3=value3; propertyKey=propertyValue'));

// Verify Sentry baggage is also present
expect(baggageHeader).toMatch(/sentry-/);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as Sentry from '@sentry/node';
import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';

export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
environment: 'prod',
// disable requests to /express
tracePropagationTargets: [/^(?!.*express).*$/],
tracesSampleRate: 1.0,
transport: loggingTransport,
});

import cors from 'cors';
import express from 'express';
import http from 'http';

const app = express();

app.use(cors());

app.get('/test/express-property-values', (req, res) => {
const incomingBaggage = req.headers.baggage;

// Forward the incoming baggage (which contains property values) to the outgoing request
// This tests that property values with = signs are preserved during parsing and re-serialization
const headers = http.get({ hostname: 'somewhere.not.sentry', headers: { baggage: incomingBaggage } }).getHeaders();

// Responding with the headers outgoing request headers back to the assertions.
res.send({ test_data: headers });
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { afterAll, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
import type { TestAPIResponse } from './server';

afterAll(() => {
cleanupChildProcesses();
});

test('should preserve baggage property values with equal signs (W3C spec compliance)', async () => {
const runner = createRunner(__dirname, 'server.ts').start();

// W3C spec example: https://www.w3.org/TR/baggage/#example
const response = await runner.makeRequest<TestAPIResponse>('get', '/test/express-property-values', {
headers: {
'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue',
},
});

expect(response).toBeDefined();

// The baggage should be parsed and re-serialized, preserving property values with = signs
const baggageItems = response?.test_data.baggage?.split(',').map(item => decodeURIComponent(item.trim()));

expect(baggageItems).toContain('key1=value1;property1;property2');
expect(baggageItems).toContain('key2=value2');
expect(baggageItems).toContain('key3=value3; propertyKey=propertyValue');
});
15 changes: 11 additions & 4 deletions packages/core/src/utils/baggage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,24 @@ export function parseBaggageHeader(
function baggageHeaderToObject(baggageHeader: string): Record<string, string> {
return baggageHeader
.split(',')
.map(baggageEntry =>
baggageEntry.split('=').map(keyOrValue => {
.map(baggageEntry => {
const eqIdx = baggageEntry.indexOf('=');
if (eqIdx === -1) {
// Likely an invalid entry
return [];
}
const key = baggageEntry.slice(0, eqIdx);
const value = baggageEntry.slice(eqIdx + 1);
return [key, value].map(keyOrValue => {
try {
return decodeURIComponent(keyOrValue.trim());
} catch {
// We ignore errors here, e.g. if the value cannot be URL decoded.
// This will then be skipped in the next step
return;
}
}),
)
});
})
.reduce<Record<string, string>>((acc, [key, value]) => {
if (key && value) {
acc[key] = value;
Expand Down
12 changes: 12 additions & 0 deletions packages/core/test/lib/utils/baggage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,16 @@ describe('parseBaggageHeader', () => {
const actual = parseBaggageHeader(input);
expect(actual).toStrictEqual(expectedOutput);
});

test('should preserve property values with equal signs', () => {
// see https://www.w3.org/TR/baggage/#example
const baggageHeader = 'key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue';
const result = parseBaggageHeader(baggageHeader);

expect(result).toStrictEqual({
key1: 'value1;property1;property2',
key2: 'value2',
key3: 'value3; propertyKey=propertyValue',
});
});
});
Loading