mswjs / msw

Industry standard API mocking for JavaScript.
https://mswjs.io
MIT License
15.98k stars 519 forks source link

msw/node unable to intercept requests from fetch in undici #2165

Closed TheHolyWaffle closed 2 months ago

TheHolyWaffle commented 6 months ago

Prerequisites

Environment check

Node.js version

20.13.1

Reproduction repository

https://github.com/TheHolyWaffle/msw-undici-reproduction

Reproduction steps

npm i npx jest to run the sample test.js unit test

Current behavior

When adding a GET handler to https://example.com, msw does not intercept the request. Instead the fetch goes to the real example.com. See failing unit test.

To make the unit test succeed, uncomment const { fetch } = require("undici"); so that the node-native fetch is used instead of the `

Expected behavior

msw should intercept the fetch call from undici and unit test should succeed.

kettanaito commented 6 months ago

Hi, @TheHolyWaffle. Thanks for reporting this.

Please, compare your usage example to the Jest and Jest+JSDOM examples from MSW. Those are functional (as functional as anything on JSDOM can get).

You don't have to import fetch from undici explicitly if you have this line.

I also highly recommend to drop jest.polyfills.js and use jest-fixed-jsdom preset instead. Try that and let me know if you still encounter the issue.

TheHolyWaffle commented 6 months ago

@kettanaito Thanks for looking into this.

I have removed the polyfills again in the minimal reproduction. They were a futile attempt to fix my issue but they never made sense. This bug purely revolves around a nodejs backend microservice, no jsdom environment is needed.

I have a hard requirement to import fetch directly from the undici package. This is because the node built-in version of fetch is from an older undici version. Since then, there have been bugs fixed in undici. Also, the node built-in fetch does not allow passing a dispatcher directly, unlike fetch from undici. Because of these reasons, I cannot use the global fetch.

Please reconsider this issue 🙏

kettanaito commented 6 months ago

I have a hard requirement to import fetch directly from the undici package.

That shouldn't be a problem.

Also, the node built-in fetch does not allow passing a dispatcher directly, unlike fetch from undici.

TIL!

No worries, let's reopen this then.

bobanm commented 4 months ago

@TheHolyWaffle we have exactly the same reasons to use undici instead of the Node's native fetch: the dispatcher. And the same problem, that MSW does not intercept and mock the requests, but let's the requests go through to the real service.

Have you found a decent solution?

TheHolyWaffle commented 4 months ago

@bobanm I went ahead with Undici's MockAgent instead of msw, see https://undici.nodejs.org/#/docs/api/MockAgent

Feel free to 👍 the original issue to bring more attention to this!

bobanm commented 4 months ago

@TheHolyWaffle I ended up doing the same. I find it to be a little bit less elegant than using MSW, but still quite a good solution.

I hope MSW adds support for Undici.

Jokinen commented 3 months ago

@TheHolyWaffle Are you sure that node's fetch does not support the dispatcher field? I have the same problem, but as far as I understand, the issue is with TS types, not in the API of the native fetch. At least the typings for the "node version of the types" do include the dispatcher field in the RequestInit object.

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/66824

If you are using typescript, the @types/node library may be giving you the DOM types when you'd prefer to use the types for node ("undici types"). If your TS project includes a frontend, you may have configured lib.dom.d.ts in use yourself ("lib": ["dom"] in tsconfig) or a dependency of yours may be pulling it in (you can try to use explainFiles to find out the cause(s) of the files inclusion if its unclear).

This may be of no real help to you. Just jotting this down for future reference in case it has an impact on API decision. Still looking for a feasible workaround myself.

E: At least one workaround is to use multiple tsconfig files and structure your project so that the fetch integration is covered by a tsconfig without lib.dom.d.ts and any dependencies that may end up including it.

kettanaito commented 3 months ago

Why this happens

Undici is a custom client that doesn't rely on the node:http module, which is a primary request-issuing module which MSW observes in Node.js (as of now). Because of that, the requests Undici makes are invisible to MSW (I believe Undici uses net.Socket directly).

How to solve this

For now, you can switch to Undici's MockAgent as Undici ships with their own mocking in mind. If that works for you, that's great!

Note that you can call MSW request handlers directly, including in your MockAgent by Undici. See the getResponse() function.

Undici support

MSW has a strong position on not shipping request client-specific code. Instead, we try out best to support the underlying protocols, tapping into the network at the layer that is both more resilient and less intrusive to the things you are running.

I highly doubt a standalone Undici support will ever happen in MSW. What may happen, is net.Socket support, which Undici uses. We've already merged the Socket-based interceptor in @mswjs/interceptors but (a) it's not used in MSW yet; (b) it still taps into node:http by using a custom Agent and a mock socket.

Raw socket-level interception is theoretically possible, we just need to get it right. Sockets transport many things, not just HTTP requests. We better not interfere with those things.

[!IMPORTANT] Also please note that MSW supports global fetch in Node.js still. That is also, technically, Undici. It just has to be a global fetch function. It's important to disambiguate that we aren't talking about global fetch but about a standalone usage of Undici, like their request function.

kettanaito commented 2 months ago

There's no plans to add raw Socket-level interception to support the direct usage of Undici. Please refer to my response above to see how you can fix it in your app.

You can also expose Undici's fetch globally as a part of your setup and have the interception working.