diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js new file mode 100644 index 000000000000..76fafc9df148 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js @@ -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', + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html new file mode 100644 index 000000000000..404eee952355 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html @@ -0,0 +1,11 @@ + + + + + + Fetch Baggage Property Values Test + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts new file mode 100644 index 000000000000..c191304ec8e0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts @@ -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-/); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js new file mode 100644 index 000000000000..839cdf137fd7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js @@ -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(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts new file mode 100644 index 000000000000..f2ac4edb4a67 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts @@ -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-/); +}); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts new file mode 100644 index 000000000000..da278ce61688 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts @@ -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://public@dsn.ingest.sentry.io/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); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts new file mode 100644 index 000000000000..23848d36a3df --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts @@ -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('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'); +}); diff --git a/packages/core/src/utils/baggage.ts b/packages/core/src/utils/baggage.ts index b483207ba8f2..e94bb3d896e6 100644 --- a/packages/core/src/utils/baggage.ts +++ b/packages/core/src/utils/baggage.ts @@ -113,8 +113,15 @@ export function parseBaggageHeader( function baggageHeaderToObject(baggageHeader: string): Record { 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 { @@ -122,8 +129,8 @@ function baggageHeaderToObject(baggageHeader: string): Record { // This will then be skipped in the next step return; } - }), - ) + }); + }) .reduce>((acc, [key, value]) => { if (key && value) { acc[key] = value; diff --git a/packages/core/test/lib/utils/baggage.test.ts b/packages/core/test/lib/utils/baggage.test.ts index 4816a3fbf079..f3717a524bf8 100644 --- a/packages/core/test/lib/utils/baggage.test.ts +++ b/packages/core/test/lib/utils/baggage.test.ts @@ -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', + }); + }); });