react-testing-librarymsw

How does fetch detect if a request using a relative path is coming from a browser?


I am making a request from a react app using a relative path "/api/test". (an NGINX controller routes traffic to either the api or the react client, so they have the same base url)

I found in the msw documentation that relative paths are supported.

// Given your application runs on "http://localhost:8080",
// this request handler URL gets resolved to "http://localhost:8080/invoices"
rest.get('/invoices', invoicesResolver)
Note that relative URL are resolved against the current location (location.origin).

If I console.log(window.location.origin), it prints http://localhost:3000. I think that the origin is being set because of import '@testing-library/jest-dom'.

However, if I make a fetch call during testing (msw is mocking api calls), I get a TypeError: Invalid URL: /api/test.

fetch('/api/test')
    .then((response) => response.json())
    .then((data) => console.log(data))
Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
TypeError: Failed to parse URL from /api/test
 ❯ new Request node:internal/deps/undici/undici:9474: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
 ❯ globalThis.fetch node_modules/@mswjs/interceptors/src/interceptors/fetch/index.ts:41:42
 ❯ App src/App.tsx:9:3
      7|
      8| function App() {
      9|   fetch('/api/test')
       |   ^
     10|     .then((response) => response.json())
     11|     .then((data) => console.log(data))
 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:16305:18

This error originated in "src/App.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
Caused by: TypeError: Invalid URL: /api/test
 ❯ new URLImpl node_modules/whatwg-url/lib/URL-impl.js:21:13
 ❯ Object.exports.setup node_modules/whatwg-url/lib/URL.js:54:12
 ❯ new URL node_modules/whatwg-url/lib/URL.js:115:22
 ❯ new Request node:internal/deps/undici/undici:9472:25
 ❯ 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

When I run the code in the browser, there are no issues. Given that document.baseURI and window.location.origin are set, how does fetch "know" it is not running in a browser? Is it possible to avoid this error by setting another field in window?


Solution

  • Rather than try to grok environment and include special behavior...

    The global fetch function in one runtime might not implement the same relative URL resolution behavior as another — especially given implicit global state (such as globalThis.window), and the fact that some environments mock the function or replace it with a custom proxy.

    In order to implement deterministic resolution, you can provide a fully-qualified address at the time of invocation — this will guarantee reliable behavior across environments. Node.js includes the same URL class that browsers do, and you can use it to construct a fully-qualified address.

    In the question details you state that window.location.origin is set to a valid URL origin string, so you can use it to resolve URLs. Below is an example that will work in Node.js and in a browser — you can run it in Node.js, or you can copy + paste the code into your dev tools JS console on this page and run it to see the same output. (I'm not posting it as a code snippet because snippets on this site run at a different origin.)

    ./example.mjs:

    /// <reference types="node" />
    
    // According to your question, this is set elsewhere in your environment:
    if (!globalThis.window) {
      globalThis.window = {
        location: {
          origin: "https://stackoverflow.com",
        },
      };
    }
    
    function resolveUrl(url) {
      return new URL(url, window.location.origin);
    }
    
    // A path relative to the current origin:
    const url1 = resolveUrl(
      "/questions/75794309/how-does-fetch-detect-if-a-request-using-a-relative-path-is-coming-from-a-browse",
    );
    
    // Address at a separate origin:
    const url2 = resolveUrl("https://developer.mozilla.org/en-US/docs/Web/API/URL");
    
    console.log("URL 1:", url1.href); // https://stackoverflow.com/questions/75794309/how-does-fetch-detect-if-a-request-using-a-relative-path-is-coming-from-a-browse
    console.log("URL 2:", url2.href); // https://developer.mozilla.org/en-US/docs/Web/API/URL
    
    const response = await fetch(url1);
    
    console.log({
      contentType: response.headers.get("Content-Type"), // text/html; charset=utf-8
      docType: (await response.text()).slice(0, 15), // <!DOCTYPE html>
    });
    
    

    In Node.js:

    % node --version      
    v18.15.0
    
    % node example.mjs
    URL 1: https://stackoverflow.com/questions/75794309/how-does-fetch-detect-if-a-request-using-a-relative-path-is-coming-from-a-browse
    URL 2: https://developer.mozilla.org/en-US/docs/Web/API/URL
    { contentType: 'text/html; charset=utf-8', docType: '<!DOCTYPE html>' }