From 7a95a8b6a9cea678a96957c743449ae524b503f7 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:53:36 +0200 Subject: [PATCH 1/3] feat(core): Add support for `x-forwarded-host` and `x-forwarded-proto` headers (#16687) Adds support for [x-forwarded-host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host) (host forwarding) and [x-forwarded-proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto) (protocol forwarding). This is useful when using proxies. Closes https://github.com/getsentry/sentry-javascript/issues/16671 Support for this was also added [in this PR for Next.js](https://github.com/getsentry/sentry-javascript/pull/16500) --- packages/core/src/utils/request.ts | 11 +- packages/core/test/lib/utils/request.test.ts | 209 +++++++++++++++++++ 2 files changed, 218 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 6c62f422207a..04cd1006ba28 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -74,8 +74,15 @@ export function httpRequestToRequestData(request: { }; }): RequestEventData { const headers = request.headers || {}; - const host = typeof headers.host === 'string' ? headers.host : undefined; - const protocol = request.protocol || (request.socket?.encrypted ? 'https' : 'http'); + + // Check for x-forwarded-host first, then fall back to host header + const forwardedHost = typeof headers['x-forwarded-host'] === 'string' ? headers['x-forwarded-host'] : undefined; + const host = forwardedHost || (typeof headers.host === 'string' ? headers.host : undefined); + + // Check for x-forwarded-proto first, then fall back to existing protocol detection + const forwardedProto = typeof headers['x-forwarded-proto'] === 'string' ? headers['x-forwarded-proto'] : undefined; + const protocol = forwardedProto || request.protocol || (request.socket?.encrypted ? 'https' : 'http'); + const url = request.url || ''; const absoluteUrl = getAbsoluteUrl({ diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts index 44682e6e9b0f..fe90578d5392 100644 --- a/packages/core/test/lib/utils/request.test.ts +++ b/packages/core/test/lib/utils/request.test.ts @@ -198,6 +198,215 @@ describe('request utils', () => { data: { xx: 'a', yy: 'z' }, }); }); + + describe('x-forwarded headers support', () => { + it('should prioritize x-forwarded-proto header over explicit protocol parameter', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'https', + }, + protocol: 'http', + }); + + expect(actual).toEqual({ + url: 'https://example.com/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'https', + }, + }); + }); + + it('should prioritize x-forwarded-proto header even when downgrading from https to http', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'http', + }, + protocol: 'https', + }); + + expect(actual).toEqual({ + url: 'http://example.com/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'http', + }, + }); + }); + + it('should prioritize x-forwarded-proto header over socket encryption detection', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'https', + }, + socket: { + encrypted: false, + }, + }); + + expect(actual).toEqual({ + url: 'https://example.com/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'https', + }, + }); + }); + + it('should prioritize x-forwarded-host header over standard host header', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'localhost:3000', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + }, + }); + + expect(actual).toEqual({ + url: 'https://example.com/test', + headers: { + host: 'localhost:3000', + 'x-forwarded-host': 'example.com', + 'x-forwarded-proto': 'https', + }, + }); + }); + + it('should construct URL correctly when both x-forwarded-proto and x-forwarded-host are present', () => { + const actual = httpRequestToRequestData({ + method: 'POST', + url: '/api/test?param=value', + headers: { + host: 'localhost:3000', + 'x-forwarded-host': 'api.example.com', + 'x-forwarded-proto': 'https', + 'content-type': 'application/json', + }, + protocol: 'http', + }); + + expect(actual).toEqual({ + method: 'POST', + url: 'https://api.example.com/api/test?param=value', + query_string: 'param=value', + headers: { + host: 'localhost:3000', + 'x-forwarded-host': 'api.example.com', + 'x-forwarded-proto': 'https', + 'content-type': 'application/json', + }, + }); + }); + + it('should fall back to standard headers when x-forwarded headers are not present', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'example.com', + }, + protocol: 'https', + }); + + expect(actual).toEqual({ + url: 'https://example.com/test', + headers: { + host: 'example.com', + }, + }); + }); + + it('should ignore x-forwarded headers when they contain non-string values', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-host': ['forwarded.example.com'] as any, + 'x-forwarded-proto': ['https'] as any, + }, + protocol: 'http', + }); + + expect(actual).toEqual({ + url: 'http://example.com/test', + headers: { + host: 'example.com', + }, + }); + }); + + it('should correctly transform localhost request to public URL using x-forwarded headers', () => { + const actual = httpRequestToRequestData({ + method: 'GET', + url: '/', + headers: { + host: 'localhost:3000', + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'example.com', + }, + }); + + expect(actual).toEqual({ + method: 'GET', + url: 'https://example.com/', + headers: { + host: 'localhost:3000', + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'example.com', + }, + }); + }); + + it('should respect x-forwarded-proto even when it downgrades from encrypted socket', () => { + const actual = httpRequestToRequestData({ + url: '/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'http', + }, + socket: { + encrypted: true, + }, + }); + + expect(actual).toEqual({ + url: 'http://example.com/test', + headers: { + host: 'example.com', + 'x-forwarded-proto': 'http', + }, + }); + }); + + it('should preserve query parameters when constructing URL with x-forwarded headers', () => { + const actual = httpRequestToRequestData({ + method: 'GET', + url: '/search?q=test&category=api', + headers: { + host: 'localhost:8080', + 'x-forwarded-host': 'search.example.com', + 'x-forwarded-proto': 'https', + }, + }); + + expect(actual).toEqual({ + method: 'GET', + url: 'https://search.example.com/search?q=test&category=api', + query_string: 'q=test&category=api', + headers: { + host: 'localhost:8080', + 'x-forwarded-host': 'search.example.com', + 'x-forwarded-proto': 'https', + }, + }); + }); + }); }); describe('extractQueryParamsFromUrl', () => { From 6c8c53b4cfd116ed68cb3aee469d937f9719587c Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 23 Jun 2025 17:12:27 +0200 Subject: [PATCH 2/3] chore: Add @sentry/opentelemetry resolution back to remix integration tests (#16692) While the remix package does not directly depend on @sentry/opentelemetry, we still need this resolution override for the integration tests. It was removed in: https://github.com/getsentry/sentry-javascript/pull/16677 --- packages/remix/test/integration/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 4e15e7de7398..7f4172b0b42f 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -27,6 +27,7 @@ "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", "@sentry/node": "file:../../../node", + "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", From 488cca2d2497f97a36c9850bc67aa8c208da14a1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 23 Jun 2025 17:16:33 +0200 Subject: [PATCH 3/3] meta(changelog): Update changelog for 9.31.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 349cd0b3ce71..9f3f00b26f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Adds the ability to pass custom `scope` and `client` parameters to the `getTrace ### Other Changes +- feat(core): Add support for `x-forwarded-host` and `x-forwarded-proto` headers ([#16687](https://github.com/getsentry/sentry-javascript/pull/16687)) - deps: Remove unused `@sentry/opentelemetry` dependency ([#16677](https://github.com/getsentry/sentry-javascript/pull/16677)) - deps: Update all bundler plugin instances to latest & allow caret ranges ([#16641](https://github.com/getsentry/sentry-javascript/pull/16641)) - feat(deps): Bump @prisma/instrumentation from 6.8.2 to 6.9.0 ([#16608](https://github.com/getsentry/sentry-javascript/pull/16608))