diff --git a/src/interceptors/fetch/index.ts b/src/interceptors/fetch/index.ts index 90c9fd5e..38e6eb90 100644 --- a/src/interceptors/fetch/index.ts +++ b/src/interceptors/fetch/index.ts @@ -7,6 +7,7 @@ import { uuidv4 } from '../../utils/uuid' import { toInteractiveRequest } from '../../utils/toInteractiveRequest' import { emitAsync } from '../../utils/emitAsync' import { isPropertyAccessible } from '../../utils/isPropertyAccessible' +import { canParseUrl } from '../../utils/canParseUrl' export class FetchInterceptor extends Interceptor { static symbol = Symbol('fetch') @@ -32,7 +33,21 @@ export class FetchInterceptor extends Interceptor { globalThis.fetch = async (input, init) => { const requestId = uuidv4() - const request = new Request(input, init) + + /** + * @note Resolve potentially relative request URL + * against the present `location`. This is mainly + * for native `fetch` in JSDOM. + * @see https://github.com/mswjs/msw/issues/1625 + */ + const resolvedInput = + typeof input === 'string' && + typeof location !== 'undefined' && + !canParseUrl(input) + ? new URL(input, location.origin) + : input + + const request = new Request(resolvedInput, init) this.logger.info('[%s] %s', request.method, request.url) diff --git a/src/utils/canParseUrl.ts b/src/utils/canParseUrl.ts new file mode 100644 index 00000000..7f814cd5 --- /dev/null +++ b/src/utils/canParseUrl.ts @@ -0,0 +1,13 @@ +/** + * Returns a boolean indicating whether the given URL string + * can be parsed into a `URL` instance. + * A substitute for `URL.canParse()` for Node.js 18. + */ +export function canParseUrl(url: string): boolean { + try { + new URL(url) + return true + } catch (_error) { + return false + } +} diff --git a/test/modules/fetch/intercept/fetch-relative-url.test.ts b/test/modules/fetch/intercept/fetch-relative-url.test.ts new file mode 100644 index 00000000..2c7c7a7c --- /dev/null +++ b/test/modules/fetch/intercept/fetch-relative-url.test.ts @@ -0,0 +1,27 @@ +/** + * @vitest-environment jsdom + */ +import { it, expect, afterAll, beforeAll } from 'vitest' +import { FetchInterceptor } from '../../../../src/interceptors/fetch' + +const interceptor = new FetchInterceptor() +interceptor.once('request', ({ request }) => { + if (request.url.endsWith('/numbers')) { + return request.respondWith(Response.json([1, 2, 3])) + } +}) + +beforeAll(() => { + interceptor.apply() +}) + +afterAll(() => { + interceptor.dispose() +}) + +it('intercepts a fetch request to a relative URL', async () => { + const response = await fetch('/numbers') + + expect(response.status).toBe(200) + expect(await response.json()).toEqual([1, 2, 3]) +})