mswjs / msw

Seamless REST/GraphQL API mocking library for browser and Node.js.
https://mswjs.io
MIT License
15.76k stars 511 forks source link

React Native Msw And Axios Not Working Together #2026

Closed batu0b closed 6 months ago

batu0b commented 8 months ago

When I make requests to msw handlers, axios returns empty strings as data for some reason, but when I make requests with fetch, the data comes correctly. when I check if the handlers are triggered, the handlers are triggered. When I make a request to another api I get data with axios. can you help me to solve the problem?

index.js

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

async function enableMocking() {
  await import('./msw.polyfills');
  const {server} = await import('./src/mocks/server');
  server.listen();
}
enableMocking();
AppRegistry.registerComponent(appName, () => App);

server.js

import {setupServer} from 'msw/native';
import {handlers} from './handlers';

export const server = setupServer(...handlers);

My Fetch Method:

const testFetch = async () => {
    try {
      const res = await axios.get('https://api.myMockApi.com/getAllProducts');
      const data = await res.data;
      console.log(data);
    } catch (error) {
      console.log(error);
    }
  };

handlers.js:

  http.get(`${url}/getAllProducts`, async ({}) => {
    console.log("test");
    return HttpResponse.json(marketData);
  }),

The Console Log Here Works Both In Fetch And In Axios

"axios": "^1.6.5"
 "react": "18.2.0",
"react-native": "0.73.4",
"fast-text-encoding": "^1.0.6",
"react-native-url-polyfill": "^2.0.0",
"msw": "^2.1.7",

Node Version : v18.13.0

AhmedBHameed commented 7 months ago

Not sure If I have the same issue but I'm encounter issue with nest.js module "@nestjs/axios".

This is some info about the console error.

  ● Notification controller › Notification controller in test mode › should call POST /api/notifications IN TEST MODE with type of LIVE_SESSION successfully

    TypeError: Cannot read properties of null (reading 'readable')

  ● Notification controller › Notification controller in test mode › should call POST /api/notifications IN TEST MODE with type of LIVE_SESSION successfully

    TypeError: body.getReader is not a function

      at _NodeClientRequest.respondWith (../../node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/NodeClientRequest.ts:555:31)
      at ../../node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/NodeClientRequest.ts:317:14
"msw": "^2.2.0",

BTW, it works fine if I return error

return new HttpResponse(null, {status: 400});
arkk200 commented 7 months ago

I have a same issue too... And it works with fetch instead of axios. I checked the headers, but in the both case, the headers were same like {"content-length": "1", "content-type": "application/json"} I think it's probably a problem with axios library.

storiesOfRen commented 7 months ago

I'm experiencing a similar issue, where it is not recognizing the axios post method, returning this error, TypeError: Right-hand side of 'instanceof' is not an object Example post setup it is erroring on: axios.post("/api/reference-url", {id: p.id, searchStr: "some word"})

Version of MSW: "msw": "^2.2.2",

renet commented 7 months ago

I also ran into axios-related issues with msw. My workaround is to mock axios to use fetch for the requests within my vitest suite. This should also be applicable to Jest (using jest.mock instead of vi.mock). Maybe that helps anyone in the future.

Solution that works for me in a test setup file:

beforeAll(() => {
  // axios needs to be mocked to use fetch in order to work with msw
  vi.mock("axios", () => {
    const isAxiosError = (error: any) => error.isAxiosError === true;
    const handleAxiosResponse = async (response: Response) => {
      if (!response.ok) {
        // eslint-disable-next-line @typescript-eslint/no-throw-literal
        throw {
          isAxiosError: true,
          message: response.statusText,
          response: {
            status: response.status,
            statusText: response.statusText,
            data: null,
            headers: {},
            config: {},
          },
        };
      }

      const data = await response.json();

      return { data };
    };
    const handleError = (error: any) => {
      throw isAxiosError(error)
        ? error
        : {
            isAxiosError: true,
            message: error.message,
          };
    };
    const cleanUpParams = (params?: Record<string, string | undefined>) => {
      return Object.fromEntries(
        Object.entries(params ?? {}).filter(
          (entry): entry is [string, string] => entry[1] !== undefined,
        ),
      );
    };
    const getMethodHandler =
      (method: string) => async (url: string, config?: any) => {
        const queryString = new URLSearchParams(
          cleanUpParams(config?.params),
        ).toString();
        const modifiedUrl = `${url}?${queryString}`;

        return await fetch(modifiedUrl, { ...config, method })
          .then(handleAxiosResponse)
          .catch(handleError);
      };
    const getMethodHandlerWithBody =
      (method: string) => async (url: string, data?: any, config?: any) => {
        const queryString = new URLSearchParams(
          cleanUpParams(config?.params),
        ).toString();
        const modifiedUrl = `${url}?${queryString}`;
        const fetchConfig = {
          ...config,
          method,
          body: data ? JSON.stringify(data) : undefined,
        };

        return await fetch(modifiedUrl, fetchConfig)
          .then(handleAxiosResponse)
          .catch(handleError);
      };

    return {
      default: {
        delete: getMethodHandler("DELETE"),
        get: getMethodHandler("GET"),
        head: getMethodHandler("HEAD"),
        patch: getMethodHandlerWithBody("PATCH"),
        post: getMethodHandlerWithBody("POST"),
        put: getMethodHandlerWithBody("PUT"),
      },
      isAxiosError,
    };
  });
});
Agent57 commented 7 months ago

I was stumped by this problem for a while.

I found that fetch was working fine, but axios just kept failing. The MSW server handler was not intercepting the network request and my axios.get() call failed with an 'ENOTFOUND' error as it couldn't resolve the sysaddrinfo() call on my dummy URL.

Eventually though, I did find a very simple solution that worked for me... I switched the import of setupServer from 'msw/native' to 'msw/node' o_0

    "axios": "^1.6.7",
    "react": "18.2.0",
    "react-native": "0.73.5"
    "jest": "^29.6.3",
    "msw": "^2.2.3",
zibs commented 6 months ago

Just want to also chime in here and say I'm seeing the same issue where the body is undefined/null when using MSW, Axios, and React Native:

"react-native": "0.73.6",
....
"apisauce": "^3.0.1", // uses axios under the hood
"axios": "^1.6.8" // bringing it in just to test with
....
"msw": "^2.2.9",

Fetch works fine! I've followed the basic set up steps in repo. I see similar comments: https://github.com/mswjs/msw/issues/1775#issuecomment-1937308589 and here: https://github.com/mswjs/msw/issues/1926#issuecomment-1937017406 (same user)

It sounds like it's somehow related to the XMLHttpRequest interceptor/stack...I do notice that it seems to be hitting the /browser codepath, and not the /node/native codepath (if that matters).

lcandiago commented 6 months ago

Same problem here. Using MSW with Axios in React Native doesn't work. The request response is not finalized and does not return the JSON as it should.

zibs commented 6 months ago

Hey @kettanaito

I've set up a basic reproducible example here (as simple as I could): https://github.com/zibs/mvce-msw-rn if you have the chance to take a look. The warning that is thrown in the video is " Cannot retrieve XMLHttpRequest response body as XML: DOMParser is not defined. You are likely using an environment that is not browser or does not polyfill browser globals correctly.", but I don't think is totally relevant (but maybe?)...

I'm happy to continue digging in, but you might be able to diagnose it much faster!

When digging I noticed that here https://github.com/mswjs/interceptors/blob/133754688adeb47cb972ab358db5e77f30084e03/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts#L335 there is never a .body in the response, but there is a ._bodyInit and ._bodyText... not sure if that's helpful/important.

Let me know if you want a separate issue created or anything.

zibs commented 6 months ago

Yeah, React Native doesn't natively support a .body on the Response object, so the interceptor will always fail. Trying to think of a solution, open to ideas!

React Native also doesn't natively support ReadableStream out of the box yet either I don't think....

akmjenkins commented 6 months ago

I've been looking at this over here: https://github.com/mswjs/msw/issues/2085 and have narrowed something similar down to a repro.

When you run msw/axios in an environment that uses the node >= 18 APIs for Request/Response, everything works. But when running in an environment that gets those APIs from elsewhere - specifically whatwg-fetch, which is common in react projects in the testing environment, not sure about react-native but I did notice the dependency in @zibs repo - then it's borked.

akmjenkins commented 6 months ago

This is not an issue with MSW, it's an issue with whatwg-fetch - I think this one - which might make it an issue to be raised over there (although the one I've referenced is closed), or perhaps an issue could be raised in react-native to use a different polyfill.

EDIT: Also same issue in RN repo. Hey, at least that one's open! EDIT 2: Possible solution SO answer

batu0b commented 6 months ago

First of all, thank you all very much for your answers, I could not follow the issues for a while. I used an alternative library as a solution, thank you.

XantreDev commented 6 months ago

Handler:

http.get('*', ({ request }) => {
    return HttpResponse.json({
      data: { bebe: 1 },
    });
  })
  LOG  13:40:52:219 [xhr:GET https://someurl.com] open GET https://someurl.com
 LOG  13:40:52:228 [xhr:GET https://someurl.com] registered event "timeout" function handleTimeout() { [bytecode] }
 LOG  13:40:52:233 [xhr:GET https://someurl.com] addEventListener timeout function handleTimeout() { [bytecode] }
 LOG  13:40:52:242 [xhr:GET https://someurl.com] setRequestHeader Accept application/json, text/plain, */*
 LOG  13:40:52:249 [xhr:GET https://someurl.com] registered event "load" function () { [bytecode] }
 LOG  13:40:52:258 [xhr:GET https://someurl.com] addEventListener load function () { [bytecode] }
 LOG  13:40:52:267 [xhr:GET https://someurl.com] converting request to a Fetch API Request...
 LOG  13:40:52:275 [xhr:GET https://someurl.com] converted request to a Fetch API Request! {"url":"https://someurl.com","credentials":"include","headers":{"map":{"accept":"application/json, text/plain, */*"}},"method":"GET","mode":null,"signal":{},"referrer":null,"bodyUsed":false,"_bodyInit":null,"_noBody":true,"_bodyText":""}
 LOG  13:40:52:284 [xhr:GET https://someurl.com] awaiting mocked response...
 LOG  13:40:52:293 [xhr:GET https://someurl.com] emitting the "request" event for 2 listener(s)...
 LOG  13:40:54:483 [xhr:GET https://someurl.com] all "request" listeners settled!
 LOG  13:40:54:495 [xhr:GET https://someurl.com] event.respondWith called with: {"type":"default","status":200,"ok":true,"statusText":"OK","headers":{"map":{"content-type":"application/json","content-length":"19"}},"url":"","bodyUsed":false,"_bodyInit":"{\"data\":{\"bebe\":1}}","_bodyText":"{\"data\":{\"bebe\":1}}"}
 LOG  13:40:54:503 [xhr:GET https://someurl.com] received mocked response: 200 OK
 LOG  13:40:54:513 [xhr:GET https://someurl.com] responding with a mocked response: 200 OK
 LOG  13:40:54:519 [xhr:GET https://someurl.com] calculated response body length 19
 LOG  13:40:54:528 [xhr:GET https://someurl.com] trigger "loadstart" {"loaded":0,"total":19}
 LOG  13:40:54:535 [xhr:GET https://someurl.com] setReadyState: 1 -> 2
 LOG  13:40:54:542 [xhr:GET https://someurl.com] set readyState to: 2
 LOG  13:40:54:548 [xhr:GET https://someurl.com] triggerring "readystatechange" event...
 LOG  13:40:54:554 [xhr:GET https://someurl.com] trigger "readystatechange"
 LOG  13:40:54:560 [xhr:GET https://someurl.com] setReadyState: 2 -> 3
 LOG  13:40:54:566 [xhr:GET https://someurl.com] set readyState to: 3
 LOG  13:40:54:573 [xhr:GET https://someurl.com] triggerring "readystatechange" event...
 LOG  13:40:54:581 [xhr:GET https://someurl.com] trigger "readystatechange"
 LOG  13:40:54:593 [xhr:GET https://someurl.com] finalizing the mocked response...
 LOG  13:40:54:603 [xhr:GET https://someurl.com] setReadyState: 3 -> 4
 LOG  13:40:54:609 [xhr:GET https://someurl.com] set readyState to: 4
 LOG  13:40:54:618 [xhr:GET https://someurl.com] triggerring "readystatechange" event...
 LOG  13:40:54:627 [xhr:GET https://someurl.com] trigger "readystatechange"
 LOG  13:40:54:634 [xhr:GET https://someurl.com] trigger "load" {"loaded":0,"total":19}
 LOG  13:40:54:643 [xhr:GET https://someurl.com] found 1 listener(s) for "load" event, calling...
 LOG  13:40:54:651 [xhr:GET https://someurl.com] getResponse (responseType: )
 LOG  13:40:54:659 [xhr:GET https://someurl.com] resolving "" response type as text
 LOG  13:40:54:666 [xhr:GET https://someurl.com] getAllResponseHeaders
 LOG  13:40:54:675 [xhr:GET https://someurl.com] resolved all response headers to content-type: application/json
content-length: 19
 LOG  13:40:54:684 [xhr:GET https://someurl.com] emitting the "response" event for 1 listener(s)...
 LOG  13:40:54:698 [xhr:GET https://someurl.com] trigger "loadend" {"loaded":0,"total":19}
 LOG  13:40:54:708 [xhr:GET https://someurl.com] found a direct "loadend" callback, calling...
 LOG  13:40:54:719 [xhr:GET https://someurl.com] getAllResponseHeaders
 LOG  13:40:54:726 [xhr:GET https://someurl.com] resolved all response headers to content-type: application/json
content-length: 19
 LOG  13:40:54:734 [xhr:GET https://someurl.com] getResponseText: "
XantreDev commented 6 months ago

I've found workaround. We can use _bodyInit if we have no body.

diff --git a/lib/browser/chunk-65PS3XCB.js b/lib/browser/chunk-65PS3XCB.js
index e4f824e7d40d3d2a48c86b2420cbfd8e7c2c53ed..07228344b25fb6b44bb396f4b9eb93332d11cac2 100644
--- a/lib/browser/chunk-65PS3XCB.js
+++ b/lib/browser/chunk-65PS3XCB.js
@@ -455,6 +455,13 @@ var XMLHttpRequestController = class {
         readNextResponseBodyChunk();
       };
       readNextResponseBodyChunk();
+    } else if (response._bodyInit) {
+      this.logger.info('mocked response has _bodyInit, faking streaming...')
+
+      const bodyInit = response._bodyInit
+      const encoder = new TextEncoder()
+      this.responseBuffer = encoder.encode(bodyInit)
+      finalizeResponse()
     } else {
       finalizeResponse();
     }

Here is fixed logs:

 LOG  14:56:48:940 [xhr:GET https://someurl.com] received mocked response: 200 OK
 LOG  14:56:48:964 [xhr:GET https://someurl.com] responding with a mocked response: 200 OK
 LOG  14:56:48:982 [xhr:GET https://someurl.com] calculated response body length 19
 LOG  14:56:49:11 [xhr:GET https://someurl.com] trigger "loadstart" {"loaded":0,"total":19}
 LOG  14:56:49:37 [xhr:GET https://someurl.com] setReadyState: 1 -> 2
 LOG  14:56:49:59 [xhr:GET https://someurl.com] set readyState to: 2
 LOG  14:56:49:99 [xhr:GET https://someurl.com] triggerring "readystatechange" event...
 LOG  14:56:49:139 [xhr:GET https://someurl.com] trigger "readystatechange"
 LOG  14:56:49:180 [xhr:GET https://someurl.com] setReadyState: 2 -> 3
 LOG  14:56:49:223 [xhr:GET https://someurl.com] set readyState to: 3
 LOG  14:56:49:262 [xhr:GET https://someurl.com] triggerring "readystatechange" event...
 LOG  14:56:49:298 [xhr:GET https://someurl.com] trigger "readystatechange"
 LOG  14:56:49:340 [xhr:GET https://someurl.com] mocked response has _bodyInit, faking streaming...
 LOG  14:56:49:387 [xhr:GET https://someurl.com] finalizing the mocked response...
 LOG  14:56:49:433 [xhr:GET https://someurl.com] setReadyState: 3 -> 4
 LOG  14:56:49:476 [xhr:GET https://someurl.com] set readyState to: 4
 LOG  14:56:49:521 [xhr:GET https://someurl.com] triggerring "readystatechange" event...
 LOG  14:56:49:566 [xhr:GET https://someurl.com] trigger "readystatechange"
 LOG  14:56:49:608 [xhr:GET https://someurl.com] trigger "load" {"loaded":19,"total":19}
 LOG  14:56:49:648 [xhr:GET https://someurl.com] found 1 listener(s) for "load" event, calling...
 LOG  14:56:49:692 [xhr:GET https://someurl.com] getResponse (responseType: )
 LOG  14:56:49:743 [xhr:GET https://someurl.com] resolving "" response type as text {"data":{"bebe":1}}
 LOG  14:56:49:785 [xhr:GET https://someurl.com] getAllResponseHeaders
 LOG  14:56:49:837 [xhr:GET https://someurl.com] resolved all response headers to content-type: application/json
content-length: 19
 LOG  14:56:49:887 [xhr:GET https://someurl.com] emitting the "response" event for 1 listener(s)...
 LOG  14:56:49:933 [xhr:GET https://someurl.com] trigger "loadend" {"loaded":19,"total":19}
 LOG  14:56:49:976 [xhr:GET https://someurl.com] found a direct "loadend" callback, calling...
 LOG  14:56:50:32 [xhr:GET https://someurl.com] getAllResponseHeaders
 LOG  14:56:50:71 [xhr:GET https://someurl.com] resolved all response headers to content-type: application/json
content-length: 19
 LOG  14:56:50:118 [xhr:GET https://someurl.com] getResponseText: "{"data":{"bebe":1}}"
XantreDev commented 6 months ago

@kettanaito Can this workaround to be useful inside interceptors package?

hardouinyann commented 5 months ago

Any fix planned ?

XantreDev commented 5 months ago

@hardouinyann you can use my patch for now

hardnold commented 5 months ago

I've found workaround. We can use _bodyInit if we have no body.

diff --git a/lib/browser/chunk-65PS3XCB.js b/lib/browser/chunk-65PS3XCB.js
index e4f824e7d40d3d2a48c86b2420cbfd8e7c2c53ed..07228344b25fb6b44bb396f4b9eb93332d11cac2 100644
--- a/lib/browser/chunk-65PS3XCB.js
+++ b/lib/browser/chunk-65PS3XCB.js
@@ -455,6 +455,13 @@ var XMLHttpRequestController = class {
         readNextResponseBodyChunk();
       };
       readNextResponseBodyChunk();
+    } else if (response._bodyInit) {
+      this.logger.info('mocked response has _bodyInit, faking streaming...')
+
+      const bodyInit = response._bodyInit
+      const encoder = new TextEncoder()
+      this.responseBuffer = encoder.encode(bodyInit)
+      finalizeResponse()
     } else {
       finalizeResponse();
     }

For anybody who comes across this, this patch needs to be applied on the @mswjs/interceptors-package, NOT msw

XantreDev commented 5 months ago

Yep that's true

prkgnt commented 5 months ago

It's working! I hope it will merge asap! thank you! @XantreDev

XantreDev commented 5 months ago

It's working! I hope it will merge asap! thank you! @XantreDev

I would like to provide a PR if @kettanaito consider it usefull

GriffinSauce commented 3 months ago

@kettanaito is this fix something you'd consider merging? (asking because this issue is closed at the moment)

I'd rather only apply a patch as a stopgap for a proper fix, otherwise MSW just isn't an option for us for now :(

gfgabrielfranca commented 2 months ago

@kettanaito, any update on this?