msw: Node interceptors don't resolve relative URLs when "location" is present

Prerequisites

Environment check

  • I’m using the latest msw version
  • I’m using Node.js version 14 or higher

Node.js version

v20.1.0

Reproduction repository

https://github.com/allevo/mock-backend-apis-reactjs

Reproduction steps

cd react-with-msw
npm ci
npm test

Current behavior

TypeError: Failed to parse URL from /api/movies
 ❯ new Request node:internal/deps/undici/undici:7084:19
 ❯ FetchInterceptor.<anonymous> node_modules/@mswjs/interceptors/src/interceptors/fetch/index.ts:42:23
 ❯ step node_modules/@mswjs/interceptors/lib/interceptors/fetch/index.js:59:23
 ❯ Object.next node_modules/@mswjs/interceptors/lib/interceptors/fetch/index.js:40:53
 ❯ node_modules/@mswjs/interceptors/lib/interceptors/fetch/index.js:34:71
 ❯ __awaiter node_modules/@mswjs/interceptors/lib/interceptors/fetch/index.js:30:12
 ❯ fetch node_modules/@mswjs/interceptors/src/interceptors/fetch/index.ts:41:42
 ❯ loadMovies src/App.tsx:18:12
     16|   function loadMovies() {
     17|     setMovies(undefined)
     18|     return fetch('/api/movies')
       |            ^
     19|       .then(res => res.json())
     20|       .then(movies => setMovies(movies))

Expected behavior

/api/movies should be mocked.

I followed this article to configure the repo https://kentcdodds.com/blog/stop-mocking-fetch

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 11
  • Comments: 20 (7 by maintainers)

Commits related to this issue

Most upvoted comments

I had a similar experience to the others trying to add MSW to an existing vitest/vue app.

As already mentioned by @hwride, the call to the Request constructor errors in https://github.com/mswjs/interceptors/blob/cf4e2284f4d2d5f6862b6a458148dff224f65038/src/interceptors/fetch/index.ts#L34

As the Request constructor in my case was the one from undici, I added undici as a dev dependency and set the global origin to window.location.href before each test (well, running it once would’ve worked as well since in our case the url does not change but better safe than sorry 😉

My code is roughly (I left out the part where I work around 946)

import { setupServer } from 'msw/node'
import { setGlobalOrigin } from 'undici'
import { beforeAll, beforeEach } from 'vitest'

export function useMockServer() {
  const server = setupServer()

  beforeAll(() => server.listen())
  beforeEach(() => {
    // Set the global origin (used by fetch) to the url provided in vitest.config.ts
    setGlobalOrigin(window.location.href)
  })
  afterAll(() => server.close())

  return { server }
}

And in vitest.config.ts

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      environment: 'jsdom',
      environmentOptions: {
        jsdom: {
          url: 'http://my-app.domain'
        }
      },
    //...
  })
)

To test:

import { useMockServer } from './useMockServer'
import { useFetch } from '@vueuse/core'
import { rest } from 'msw'
import { beforeEach, describe, expect, test } from 'vitest'

const { server } = useMockServer()
const body = { message: 'Success!' }

describe('testMsw', () => {
  beforeEach(() => {
    server.use(
      rest.get('/api/message', (_req, res, context) => {
        return res(context.json(body))
      })
    )
  })

  describe('when using a relative URL', () => {
    test('fetch works ', async () => {
      const response = await fetch('/api/message')
      expect(await response.json()).toEqual(body)
    })

    test('useFetch works', async () => {
      const { data } = await useFetch('/api/message').json()
      expect(data.value).toEqual(body)
    })
  })

  describe('when using an absolute URL', () => {
    test('fetch works', async () => {
      const response = await fetch('http://my-app.domain/api/message')
      expect(await response.json()).toEqual(body)
    })

    test('useFetch works', async () => {
      const { data } = await useFetch('http://my-app.domain/api/message').json()
      expect(data.value).toEqual(body)
    })
  })
})

This is quite a blocker. Modifying fetch call side is not a real solution.

I am also experiencing this issue in my test setup. The issue seems to be on the MSW side. It is not resolving urls that are being called in the client-side code, even though the location global is set properly in the JSDom environment. MSW doesn’t seem to be resolving the relative urls at all. It doesn’t seem to recognize the relative URLs that are being used in the client-side code.

FYI - Here’s an Minimal Complete Verifiable Example (MCVE). In this case, jsdom is loaded via pragma and adds window.location to the environment, but fetch still requires the absolute URL

npm i msw vitest
// @vitest-environment jsdom
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { setupServer } from "msw/node";
import { rest } from "msw";

const server = setupServer(
    rest.get("/person", (req, res, ctx) => {
        return res(
            ctx.status(200),
            ctx.json(["Kyle"]));
    }),
);

async function getPerson() {
    const resp = await fetch(`/person`);
    var data = await resp.json();
    return data;
}

describe("index", () => {
    beforeAll(() => server.listen());
    afterAll(() => server.close());

    it("should be true", () => {
        expect(true).toBe(true);
    });

    it("should have window.location", () => {
        expect(window.location.origin).toBe("http://localhost:3000");
    });

    it("should fetch", async () => {
        var data = await getPerson();
        expect(data[0]).toBe("Kyle");
    });
})
npx vitest run

And you can fix the tests by including the origin in the url

- await fetch(`/person`);
+ await fetch(`${window.location.origin}/person`);

@LuanScudeler, that can certainly work as well! However, it’s okay to request relative resources in the client-side code. I don’t believe that needs any adjustments. The pickle here is configuring the test environment, which is Node.js, where relative URLs do not exist.

This may as well be a bug on our side, we should pick up and respect the location global. We do so in the source code but, apparently, there are scenarios when this doesn’t work as expected:

https://github.com/mswjs/interceptors/blob/ca47c880790cabab58e667d1a81a82d25ab31a33/src/interceptors/fetch/index.ts#L51-L54

Source code of the @mswjs/interceptors@0.17.x (backport).

Someone would have to look into the reported reproduction repository and see what’s going on.

This may as well be a bug on our side, we should pick up and respect the location global. We do so in the source code but, apparently, there are scenarios when this doesn’t work as expected:

https://github.com/mswjs/interceptors/blob/ca47c880790cabab58e667d1a81a82d25ab31a33/src/interceptors/fetch/index.ts#L51-L54

Source code of the @mswjs/interceptors@0.17.x (backport).

Someone would have to look into the reported reproduction repository and see what’s going on.

So for my side, the error is throw before it gets to that line, at the Request constructor:

TypeError: Failed to parse URL from /api/signup at new Request (node:internal/deps/undici/undici:7043:19) at FetchInterceptor.<anonymous> 
   (.../node_modules/@mswjs/interceptors/src/interceptors/fetch/index.ts:42:23) at step

See here: https://github.com/mswjs/interceptors/blob/ca47c880790cabab58e667d1a81a82d25ab31a33/src/interceptors/fetch/index.ts#L42

Where my fetch call is: fetch('/api/signup')

Possibly the fix is moving the location based URL normalising above the new Request call, and passing the normalised URL to new Request as well as to new IsomorphicRequest, where as at the moment it’s only passed to new IsomorphicRequest. Something like:

      const url = typeof input === 'string' ? input : input.url
      const requestUrl = new URL(
        url,
        typeof location !== 'undefined' ? location.origin : undefined
      )
      const request = new Request(requestUrl, init)
      const method = request.method

Hi, I’m bit confused about this topic. I was having the same issue as @allevo, but I’ve solved it by adding the hostname to the actual Fetch API call and not in the MSW request handler call, so now the working setup I have is something like this:

const baseUrl = import.meta.env.MODE === 'test' ? 'http://localhost:8080' : ''
const fetchMovies = () => {
    fetch(`${HOSTNAME}/api/movies`).then(...
}

export const handlers = [
  rest.post('/api/movies', async (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({}))
  }),
]

Doesn’t this means that MSW request handler is fine with relative URLs and then something else is not resolving relative URLs? Maybe node-fetch?

Hi! Thanks again for your patience. The test

    console.log(window.location.href)
    console.log(location.href)

prints correctly

http://localhost:3000/
http://localhost:3000/

So don’t understand where is the mistake