From 4400eebb9e45498b9f8e65a4225c8d615bde3cf9 Mon Sep 17 00:00:00 2001 From: Kunal Vashisht Date: Tue, 20 Oct 2026 07:51:01 +0000 Subject: [PATCH 1/4] fix(core): Resolve relative URLs when enhancing fetch error messages --- packages/core/src/instrument/fetch.ts | 18 ++++++++++-------- packages/core/src/utils/worldwide.ts | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/instrument/fetch.ts b/packages/core/src/instrument/fetch.ts index 443c71095e67..40f68dc7e887 100644 --- a/packages/core/src/instrument/fetch.ts +++ b/packages/core/src/instrument/fetch.ts @@ -136,16 +136,18 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void): void { error.message === 'NetworkError when attempting to fetch resource.') ) { try { - const url = new URL(handlerData.fetchData.url); + const url = new URL(handlerData.fetchData.url, GLOBAL_OBJ.location?.origin); const hostname = url.host; - if (enhanceOption === 'always') { - // Modify the error message directly - error.message = `${error.message} (${hostname})`; - } else { - // Store hostname as non-enumerable property for Sentry-only enhancement - // This preserves the original error message for third-party packages - addNonEnumerableProperty(error, '__sentry_fetch_url_host__', hostname); + if (hostname) { + if (enhanceOption === 'always') { + // Modify the error message directly + error.message = `${error.message} (${hostname})`; + } else { + // Store hostname as non-enumerable property for Sentry-only enhancement + // This preserves the original error message for third-party packages + addNonEnumerableProperty(error, '__sentry_fetch_url_host__', hostname); + } } } catch { // ignore it if errors happen here diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index 597174c3f94d..aa52e9808d85 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -19,6 +19,7 @@ import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ export type InternalGlobal = { navigator?: { userAgent?: string; maxTouchPoints?: number }; + location?: { origin?: string }; console: Console; PerformanceObserver?: any; Sentry?: any; From 28942cc0bb4085a5403db3cee1e965d346cd2679 Mon Sep 17 00:00:00 2001 From: Kunal Vashisht Date: Tue, 20 Oct 2026 07:53:55 +0000 Subject: [PATCH 2/4] test: unit tests added for relative URL, an empty URL, no , a page, and an absolute URL on a different origin than the page --- .../core/test/lib/instrument/fetch.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/core/test/lib/instrument/fetch.test.ts b/packages/core/test/lib/instrument/fetch.test.ts index e0f31ca02c82..728179145cef 100644 --- a/packages/core/test/lib/instrument/fetch.test.ts +++ b/packages/core/test/lib/instrument/fetch.test.ts @@ -121,4 +121,49 @@ describe('instrument > addFetchInstrumentationHandler', () => { expect(error.message).toBe('Failed to fetch (example.com)'); }); + + describe('relative URLs', () => { + const globalWithLocation = GLOBAL_OBJ as typeof GLOBAL_OBJ & { location?: { origin: string } }; + + afterEach(() => { + Reflect.deleteProperty(globalWithLocation, 'location'); + }); + + async function failWith(target: string): Promise { + const error = new TypeError('Failed to fetch'); + globalThis.fetch = vi.fn().mockRejectedValue(error); + addFetchInstrumentationHandler(() => undefined); + await expect(globalThis.fetch(target)).rejects.toBe(error); + return error; + } + + it('resolves a relative URL against the current origin', async () => { + globalWithLocation.location = { origin: 'https://app.example.com/some/page?x=1' }; + expect((await failWith('/api/users')).message).toBe('Failed to fetch (app.example.com)'); + }); + + it('resolves an empty URL (`fetch("")`) against the current page', async () => { + globalWithLocation.location = { origin: 'https://app.example.com/some/page?x=1' }; + expect((await failWith('')).message).toBe('Failed to fetch (app.example.com)'); + }); + + it('does not change the message for a relative URL if there is no current page', async () => { + expect((await failWith('/api/users')).message).toBe('Failed to fetch'); + }); + + it('does not append an empty host (e.g. pages served from file://)', async () => { + globalWithLocation.location = { origin: 'file:///C:/app/index.html' }; + expect((await failWith('/api/users')).message).toBe('Failed to fetch'); + }); + + it('does not change the message if the origin is opaque ("null")', async () => { + globalWithLocation.location = { origin: 'null' }; + expect((await failWith('/api/users')).message).toBe('Failed to fetch'); + }); + + it('still uses the host of an absolute URL, ignoring the current page', async () => { + globalWithLocation.location = { origin: 'https://app.example.com/some/page' }; + expect((await failWith('https://other.example.org/x')).message).toBe('Failed to fetch (other.example.org)'); + }); + }); }); From be0b69e9142f21ce977ae5468cdd5b3c0c4dfd44 Mon Sep 17 00:00:00 2001 From: Kunal Vashisht Date: Tue, 20 Oct 2026 16:42:10 +0000 Subject: [PATCH 3/4] fix(core): Resolve relative URLs when enhancing fetch error messages --- packages/core/src/instrument/fetch.ts | 2 +- packages/core/src/utils/worldwide.ts | 2 +- .../core/test/lib/instrument/fetch.test.ts | 45 ++++++++----------- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/packages/core/src/instrument/fetch.ts b/packages/core/src/instrument/fetch.ts index 40f68dc7e887..613262caee67 100644 --- a/packages/core/src/instrument/fetch.ts +++ b/packages/core/src/instrument/fetch.ts @@ -136,7 +136,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void): void { error.message === 'NetworkError when attempting to fetch resource.') ) { try { - const url = new URL(handlerData.fetchData.url, GLOBAL_OBJ.location?.origin); + const url = new URL(handlerData.fetchData.url, GLOBAL_OBJ.location?.href); const hostname = url.host; if (hostname) { diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index aa52e9808d85..2ca3d60bc21d 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -19,7 +19,7 @@ import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ export type InternalGlobal = { navigator?: { userAgent?: string; maxTouchPoints?: number }; - location?: { origin?: string }; + location?: { href?: string }; console: Console; PerformanceObserver?: any; Sentry?: any; diff --git a/packages/core/test/lib/instrument/fetch.test.ts b/packages/core/test/lib/instrument/fetch.test.ts index 728179145cef..e094f8c46760 100644 --- a/packages/core/test/lib/instrument/fetch.test.ts +++ b/packages/core/test/lib/instrument/fetch.test.ts @@ -123,7 +123,7 @@ describe('instrument > addFetchInstrumentationHandler', () => { }); describe('relative URLs', () => { - const globalWithLocation = GLOBAL_OBJ as typeof GLOBAL_OBJ & { location?: { origin: string } }; + const globalWithLocation = GLOBAL_OBJ as typeof GLOBAL_OBJ & { location?: { href: string } }; afterEach(() => { Reflect.deleteProperty(globalWithLocation, 'location'); @@ -137,33 +137,24 @@ describe('instrument > addFetchInstrumentationHandler', () => { return error; } - it('resolves a relative URL against the current origin', async () => { - globalWithLocation.location = { origin: 'https://app.example.com/some/page?x=1' }; - expect((await failWith('/api/users')).message).toBe('Failed to fetch (app.example.com)'); - }); - - it('resolves an empty URL (`fetch("")`) against the current page', async () => { - globalWithLocation.location = { origin: 'https://app.example.com/some/page?x=1' }; - expect((await failWith('')).message).toBe('Failed to fetch (app.example.com)'); - }); - - it('does not change the message for a relative URL if there is no current page', async () => { - expect((await failWith('/api/users')).message).toBe('Failed to fetch'); - }); - - it('does not append an empty host (e.g. pages served from file://)', async () => { - globalWithLocation.location = { origin: 'file:///C:/app/index.html' }; - expect((await failWith('/api/users')).message).toBe('Failed to fetch'); - }); - - it('does not change the message if the origin is opaque ("null")', async () => { - globalWithLocation.location = { origin: 'null' }; - expect((await failWith('/api/users')).message).toBe('Failed to fetch'); - }); + it.each([ + ['a relative path', 'https://app.example.com/some/page?x=1', '/api/users', 'Failed to fetch (app.example.com)'], + ['an empty URL (`fetch("")`)', 'https://app.example.com/some/page?x=1', '', 'Failed to fetch (app.example.com)'], + ['a relative path with no current page', undefined, '/api/users', 'Failed to fetch'], + ['a relative path on a `file://` page', 'file:///C:/app/index.html', '/api/users', 'Failed to fetch'], + ['a relative path with an opaque origin', 'null', '/api/users', 'Failed to fetch'], + [ + 'an absolute URL, ignoring the current page', + 'https://app.example.com/some/page', + 'https://other.example.org/x', + 'Failed to fetch (other.example.org)', + ], + ])('handles %s', async (_name, href, target, expected) => { + if (href) { + globalWithLocation.location = { href }; + } - it('still uses the host of an absolute URL, ignoring the current page', async () => { - globalWithLocation.location = { origin: 'https://app.example.com/some/page' }; - expect((await failWith('https://other.example.org/x')).message).toBe('Failed to fetch (other.example.org)'); + expect((await failWith(target)).message).toBe(expected); }); }); }); From 6478625fd084c1deb6590424f50c3b054582c06c Mon Sep 17 00:00:00 2001 From: Kunal Vashisht Date: Wed, 21 Oct 2026 16:09:18 +0000 Subject: [PATCH 4/4] test(core): add sandboxed iframe test cases for fetch --- packages/core/test/lib/instrument/fetch.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/test/lib/instrument/fetch.test.ts b/packages/core/test/lib/instrument/fetch.test.ts index e094f8c46760..68252f99485a 100644 --- a/packages/core/test/lib/instrument/fetch.test.ts +++ b/packages/core/test/lib/instrument/fetch.test.ts @@ -142,7 +142,13 @@ describe('instrument > addFetchInstrumentationHandler', () => { ['an empty URL (`fetch("")`)', 'https://app.example.com/some/page?x=1', '', 'Failed to fetch (app.example.com)'], ['a relative path with no current page', undefined, '/api/users', 'Failed to fetch'], ['a relative path on a `file://` page', 'file:///C:/app/index.html', '/api/users', 'Failed to fetch'], - ['a relative path with an opaque origin', 'null', '/api/users', 'Failed to fetch'], + ['a relative path in a sandboxed srcdoc iframe', 'about:srcdoc', '/api/users', 'Failed to fetch'], + [ + 'an absolute URL in a sandboxed srcdoc iframe', + 'about:srcdoc', + 'https://api.example.org/v1/users', + 'Failed to fetch (api.example.org)', + ], [ 'an absolute URL, ignoring the current page', 'https://app.example.com/some/page',