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
?
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>' }