msw icon indicating copy to clipboard operation
msw copied to clipboard

React Native Msw And Axios Not Working Together

Open batu0b opened this issue 1 year ago • 1 comments

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

batu0b avatar Feb 08 '24 17:02 batu0b

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});

AhmedBHameed avatar Feb 14 '24 19:02 AhmedBHameed

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.

arkk200 avatar Mar 02 '24 12:03 arkk200

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",

storiesOfRen avatar Mar 06 '24 21:03 storiesOfRen

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,
    };
  });
});

renet avatar Mar 07 '24 10:03 renet

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",

Agent57 avatar Mar 12 '24 03:03 Agent57

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).

zibs avatar Mar 21 '24 16:03 zibs

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.

lcandiago avatar Mar 21 '24 20:03 lcandiago

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 avatar Mar 22 '24 17:03 zibs

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....

zibs avatar Mar 27 '24 20:03 zibs

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 avatar Mar 28 '24 12:03 akmjenkins

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

akmjenkins avatar Mar 29 '24 23:03 akmjenkins

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.

batu0b avatar Mar 30 '24 02:03 batu0b

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 avatar Apr 10 '24 11:04 XantreDev

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 avatar Apr 10 '24 12:04 XantreDev

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

XantreDev avatar Apr 10 '24 13:04 XantreDev

Any fix planned ?

hardouinyann avatar Apr 12 '24 14:04 hardouinyann

@hardouinyann you can use my patch for now

XantreDev avatar Apr 12 '24 15:04 XantreDev

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

hardnold avatar Apr 18 '24 09:04 hardnold

Yep that's true

XantreDev avatar Apr 18 '24 11:04 XantreDev

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

prkgnt avatar May 04 '24 15:05 prkgnt

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

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

XantreDev avatar May 04 '24 19:05 XantreDev

@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 :(

GriffinSauce avatar Jun 13 '24 07:06 GriffinSauce

@kettanaito, any update on this?

gfgabrielfranca avatar Jul 29 '24 19:07 gfgabrielfranca