Open kettanaito opened 9 months ago
I got the server-side MSW integration working in Next.js by using the instrumentation
hook:
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = await import('./mocks/node')
server.listen()
}
}
This allows MSW to intercept server-side requests Next.js makes.
mocks/handlers.ts
because nothing but the instrumentation hook depends on that import. This means stale mocks until you re-run Next.js/force it re-evaluate the instrumentation hook. I'm our company we are using this example as a reference.
@SalahAdDin
I'm our company we are using this example as a reference.
is it working? I've tried to use, but it's showing these messages below:
Internal error: TypeError: fetch failed
`[MSW] Warning: intercepted a request without a matching request handler:
@SalahAdDin
I'm our company we are using this example as a reference.
Is it working? I've tried to use it, but it's showing these messages below:
Internal error: TypeError: fetch failed
`[MSW] Warning: intercepted a request without a matching request handler:
Not checked it yet, We just set it up.
@kettanaito I don't know why but MSW is not intercepting page request. The mock is enabled but does not catch any fetch.
how does playwright work with this? My test makes the actual api call instead of the mocked call
@pandeymangg, there should be nothing specific to Playwright here. You enable MSW in your Next.js app, then navigate to it in a Playwright test and perform the actions you need.
Hey @kettanaito what is the status of this example? 😄
Got some of this working in my own app, but curious to see the libraries official recommended approach. Thanks 🙏
PS. love the library, it rivals trpc and react query as my favourite open source projects.
@pandeymangg, the Playwright integration is the same as in this example:
There has never been anything specific to Cypress/Playwright that MSW required.
@mw999, that is a high praise, thank you! No updates on this example, I didn't have time to look into it and, frankly, very little I can do here. If Next.js doesn't pick up changes from the instrumentation hook in HRM, I cannot fix that. I won't be recommending half-baked experiences. For now, you cannot officially use MSW in Next.js.
This is great work, thanks @kettanaito. Have you looked into the work that has been done here? https://github.com/vercel/next.js/tree/canary/packages/next/src/experimental/testmode/playwright
There may be an opportunity to work more closely with the Next.js team on this functionality, especially for use cases like Playwright.
One question, with this template, how i can toggle msw with playwright to run only on test env?
@mrmckeb, thank you. Yes, I've heard about the test mode in Next.js. I don't believe it's the right place to integrate MSW. I will look at it again in the future but it's unlikely I will be recommending it.
@bertoldoklinger, you can toggle it by introducing an environment variable. Start the worker conditionally based on the value of that variable.
// your/app.jsx
if (process.env.SOME_VARIABLE === 'some-value') {
// ...
worker.start()
}
I'm using this config with vitest
, but If I try to run a simple test:
test('should pass', function () {
expect(true).toBe(true);
});
I get:
Error: No known conditions for "./browser" specifier in "msw" package
Some suggestion to avoid this issue?
@crisfcodes, you need to configure your Vitest not to load browser
modules in Node.js. Afaik, it doesn't do that by default. Perhaps you've enabled that explicitly?
I'm using jsdom
env in Vitest, if that is what you mean with enabled
, also I'm wrapping the app with the MockProvider
but the worker.start
executed conditionally. what I did as workaround is to ignore the file that is throwing the error(browser.ts
) with Vitest exclusions.
Also I'm having another issues like:
browser/index.js
file... I patched the package adding ?
to request?.url
and that fixed the issue.MockProvider reference:
import { PropsWithChildren, useEffect, useState } from 'react';
import { env } from '@/constants/env.mjs';
export const MockProvider = ({ children }: PropsWithChildren) => {
const [mockingEnabled, enableMocking] = useState(false);
const isProduction = env.NEXT_PUBLIC_ENV === 'prod';
const isAppReady = mockingEnabled || isProduction;
useEffect(() => {
const enableApiMocking = async () => {
const isLocalDevelopment = env.NEXT_PUBLIC_ENV === 'local';
const shouldEnableMocking = typeof window !== 'undefined' && isLocalDevelopment;
if (shouldEnableMocking) {
const { worker } = await import('../tests/mocks/browser');
await worker.start({
serviceWorker: {
url: `${env.NEXT_PUBLIC_BASE_PATH}/mockServiceWorker.js`,
},
});
enableMocking(true);
}
};
enableApiMocking();
}, []);
if (!isAppReady) {
return null;
}
return <>{children}</>;
};
Sorry to use this comment for pointing to other issues, I'm just posting what I've got using this code
Does anyone have any example about middleware?
FYI the current example doesn't seem to work when using Turbopack on Next 14 or RC.0 of Next 15. However it works great with the exact version in the package.json here.
Hi I know that this PR is still in progress - I happen to be investigating how to using MSW with NextJS - it appears that this example as it currently is has the same problem that I've run into - MSW mocks seem to work for the first time the endpoint is called, but for subsequent calls the MSW behaviour is lost.
(I get this error message in subsequent calls):
SERVER LISTEN
is fetch patched? YES
GET / 200 in 3124ms
is fetch patched? undefined
⨯ TypeError: fetch failed
at async getUser (./app/page.tsx:15:22)
at async Home (./app/page.tsx:20:18)
digest: "2377642097"
Cause: Error: getaddrinfo ENOTFOUND api.example.com
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:108:26)
at GetAddrInfoReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'api.example.com'
}
GET / 200 in 67ms
Is this maybe something to do with how NextJS is munging fetch for its caching?
Update: short answer is YES.
If we replace our getUser with a call using axios
async function getUser() {
console.log('is fetch patched?', Reflect.get(fetch, '__FOO'))
const result = await axios.get('https://api.example.com/user');
const user = result.data as User;
return user
}
Then our mocking behaviour works fine.
Is it possible to completely opt out of Next's fetch caching?
Server-side integration
I got the server-side MSW integration working in Next.js by using the
instrumentation
hook:export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { const { server } = await import('./mocks/node') server.listen() } }
This allows MSW to intercept server-side requests Next.js makes.
Downsides
- Next seems to evaluate the instrumentation hook once. The import graph it creates will not update if you change
mocks/handlers.ts
because nothing but the instrumentation hook depends on that import. This means stale mocks until you re-run Next.js/force it re-evaluate the instrumentation hook.
To add to this:
In my testing, for some strange reason a guard clause doesn't work. That is:
// Fails
export const register = async (): Promise<void> => {
if (process.env.NEXT_RUNTIME !== 'nodejs') {
return;
}
const { server } = await import('./mocks/node');
server.listen();
};
// Succeeds
export const register = async (): Promise<void> => {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = await import('./mocks/node');
server.listen();
}
};
@PhilipAngelinNE, I bet that guard doesn't work because expressions like process.env.NEXT_RUNTIME !== 'nodejs'
are evaluated at build-time, and if its false, the entire if
statement will be removed. That's a wild guess.
I confirm that https://github.com/vercel/next.js/pull/68193 fixes the remaining server-side issues, and renders a great experience! 🎉 Huge thanks to @feedthejim for his help with this one.
Another pending issue is this: https://github.com/vercel/next.js/issues/69098. Hope to get some feedback on it and help how I can. Looking good.
I believe I've tracked down the root cause of the remaining browser HMR issue. See my investigation here: https://github.com/vercel/next.js/issues/69098#issuecomment-2317652004.
While I'm illustrating how to "fix" that issue in the comment, note that it is a workaround, and it is not recommended. Ultimately, it mustn't mater where you import your handlers. It's up to the framework's bundler to provide the correct update path for modules during HMR.
Pinging the Next.js team for more assistance now that the root cause is more clear.
I've been using this as a guide to getting MSW set up with Playwright in Next - thank you so much!
I'm just wondering if it's possible to override the mocked responses inside specific Playwright tests using server.use()
. I've exposed the server as a fixture which can be consumed in the Playwright tests, but the handlers I create never seem to be called.
I'm wondering if the server instance being used in the Playwright tests could potentially be different to the one initialised in the root layout.tsx
file, but I'm not sure how to check this. Any ideas would be very welcome!
@jogilvyt, glad to hear that. Playwright runs in 2 environments: your tests run in Node.js, your app runs in the browser. You want to use setupWorker
in the browser. The setupServer
you introduce as a fixture in your tests never affects the browser process, only the Node.js (test) process.
What you are describing will be possible with https://github.com/mswjs/msw/pull/1617 once it ships. Stay tuned. You are also welcome to support that effort financially if your team relies on this feature. Thanks.
Thanks @kettanaito - that makes sense, and yes setupRemoteServer
sounds like exactly what I'm looking for. I'll keep an eye on the PR ❤️
HI, I'm using next 14 and I can't integrate with mws on server Module not found: Can't resolve '_http_common'
, someone had this same error?
HI, I'm using next 14 and I can't integrate with mws on server
Module not found: Can't resolve '_http_common'
, someone had this same error?
Does anyone have any example about middleware?
MSW 2.0 is not working with middlewares I have an error:
Invariant Violation: Failed to create a WebStorageCookieStore: `localStorage` is not available in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues
at <unknown> (webpack-internal:///(instrument)/./node_modules/outvariant/lib/index.mjs:75)
at eval (webpack-internal:///(instrument)/./node_modules/msw/lib/core/utils/cookieStore.mjs:147:108)
@pstachula-dev, that looks like a rather old version of MSW. npm i msw@latest
, and let me know.
@kettanaito It was MSW 2.4.3
on the latest version 2.4.9
I have different error:
Module not found: Can't resolve '_http_common'
https://nextjs.org/docs/messages/module-not-found
@pstachula-dev, for that you have to wait for Next.js to release that bugfix. It's already been merged (see https://github.com/vercel/next.js/issues/70262).
@kettanaito Update...
Test repo: https://github.com/pstachula-dev/msw-nextjs-error
MSW: 2.4.9
Nextjs: 15.0.0-canary.166
Module not found: Can't resolve '_http_common'
- this errors is gone 🆗
But now I have new problems:
Compiled /_not-found in 1218ms (920 modules)
⨯ node_modules/outvariant/lib/index.mjs (69:1) @ <unknown>
⨯ Failed to create a WebStorageCookieStore: `localStorage` is not available in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues
67 | var invariant = (predicate, message, ...positionals) => {
68 | if (!predicate) {
> 69 | throw new InvariantError(message, ...positionals);
| ^
70 | }
71 | };
72 | invariant.as = (ErrorConstructor, predicate, message, ...positionals) => {
@pstachula-dev, can you please provide a reproduction repo for this?
The error means you are running browser code in a non-browser environment. Properly describing what you are doing, how, and what you expect as a result will help tremendously.
@kettanaito I have already posted repository above :)
Test repo: https://github.com/pstachula-dev/msw-nextjs-error
@pstachula-dev I upgraded to v15.0.0-canary.171, and I don't see any errors now
@felipepalazzo The situation is quite dynamic, this version is from 17h ago 😆
Hmm I have still same problems: with canary.171. Node 20.17.0
⨯ node_modules/outvariant/lib/index.mjs (69:1) @ <unknown>
⨯ Failed to create a WebStorageCookieStore: `localStorage` is not available in this environment. This is likely an issue with MSW. Please report it on GitHub: https://github.com/mswjs/msw/issues
67 | var invariant = (predicate, message, ...positionals) => {
68 | if (!predicate) {
> 69 | throw new InvariantError(message, ...positionals);
| ^
70 | }
71 | };
72 | invariant.as = (ErrorConstructor, predicate, message, ...positionals) => {
As a workaround, to fix HMR not disposing of the registered worker, we can tell the HMR code how to dispose of it ourselves.
In browser.ts
, append to the end of the file
module.hot?.dispose(() => { worker.stop(); })
You may need a ts-expect-error
.
What this is doing is adding the function () => { worker.stop(); }
to a list of functions called when the module is disposed of (in this case since it's being replaced). This makes it so client-side HMR is working as expected (at least with minimal testing so far).
Originally mentioned here: https://github.com/vercel/next.js/issues/69098
@sebws, still curious how does calling worker.stop()
help?
All worker.stop()
does it tells the worker script that this client has to be ignored. It doesn't clean up the handlers, doesn't affect the worker's state otherwise. I think what worker.stop()
achieves in your suggest is ignores the previous, persisted worker instance from the last refresh so it doesn't conflict with the newly created instance after a new refresh (HMR). Thus providing once again that it's a workaround.
@kettanaito I'll have to double check. I remember the worker no longer being present in the memory of the page.
There's a chance I misunderstood if it was working correctly, so I was hoping you might give it a shot.
Yep, as far as I can see without it, there'd be extra SetupWorkerApi
s, when taking a heap snapshot in the memory tab of chrome devtools.
With the workaround, there's just the one (a new one each reload). My guess would be that if worker.stop()
doesn't kill the worker, whatever it does do, removes the retainers of the worker that otherwise keep it in memory.
Adds a Next.js 14 (App directory ) + MSW usage example.
Todos