Msw: Chrome Dev tools XHR

Created on 2 Oct 2020  路  11Comments  路  Source: mswjs/msw

Describe the bug

Not sure if it is even a msw problem, but maybe someone knows the answer. For some reason, when msw is enabled, I can not set up a filter to see only XHR requests in Chrome network tab. See how it looks like below (note, that static files are displayed even though the XHR filter is on.)

Environment

  • msw: 0.21.2
  • nodejs: 12.16.1
  • npm: 6.14.8
  • Chrome Version 85.0.4183.121 (Official Build) (64-bit)

To Reproduce

Steps to reproduce the behavior:

  1. Set up msw for browser as described in official docs
  2. Open Chrome Dev Tools
  3. Turn on filter XHR
  4. Reload the page.
  5. You will see all requests.

Expected behavior

Only XHR requests are displayed

Screenshots

Screenshot 2020-10-02 at 15 33 02

bug browser

Most helpful comment

During the work on this I've found out a few details that I'd like to share.

Short answer

No, this won't be possible to do due to the Service Worker specification.

Long answer

The worker's "fetch" event triggers for any HTTP request from the client once the worker is active. The fetch event handler is a _synchronous function_, which may call event.respondWith() to respond with any Response, but it must call it synchronously:

self.addEventListener('fetch', function (event) {
  event.respondWith(anyPromise) // OK

  await somePromise()
  event.respondWith(...) // INVALID STATE
})

So there's the first behavior we learn:

  • event.respondWith() must be called synchronously in the "fetch" event handler.

To fetch data in this worker's event handler you can use the fetch() API. Since it's async, we can only use it _within_ the respondWith() Promise:

event.respondWith(async () => {
  return fetch('...').then(data => data.json())
})

Whenever you perform a fetch() call within the worker's scope it gets registered as the XHR in the Network tab. That is the behavior one cannot change. Thus, the next discovery:

  • fetch() calls in the worker's scope are always registered as XHR

Now let's analyze what happens to a request. Specifically, to a static asset request that shouldn't be mocked. Here's the journey it undergoes:

  1. Client loads its JavaScript code.
  2. The JS code registers the worker.
  3. The JS code performs an HTTP request.
  4. Request is caught by the "fetch" event of the worker.
  5. Worker signals to MSW to see if this request should be mocked (async action).
  6. If not, it performs request as is via event.respondWith(fetch(event.request)).

Since the step 5 is async, it must be nested in the event.respondWith() promise. If the request shouldn't be mocked, to perform an original request one needs to call fetch(event.request), which always produces an XHR request. This means that the requests that should be bypassed are performed as XHR and you can see them twice in your browser's Network tab.

There are much more nuances to the async worker behavior. For instance, there is a time window between steps 2 and 3 and if a request happens there it's _always_ bypassed, as that means "the worker is not ready to signal to MSW". _Those_ requests will not be listed as XHR, because the fetch event handler short circuits on those requests _outside_ of the event.respondWith() function call.

If this request duplication in the Network is confusing, this is how you can think of it:

  1. First (non XHR) request is what your app makes.
  2. Second (XHR) request is what the worker makes in order to fetch the original (bypassed) respond data and use it to respond to your first request.

All 11 comments

Hi @malykhinvi thanks for reaching us 馃槃 .

I have to be honest, I didn't notice that 馃槅 . BTW I can say that Service Workers are like a proxy. So in your DevTools you should see requests two times. I'm pretty sure it is a normal. You should consider also that the Service Workers will listen for fetch events. Also static files are intercepted by worker, but for those files the request is not forwarded to MSW

Just installed msw and it's the first thing I noticed. Static assets are displayed twice, once where they should and once as XHR requests. It's causing some confusion.

That's the behavior caused by the current implementation of request handling in the worker. As the worker persists between pages, when the app loads the worker handles _all_ requests. Since you rarely want to mock a static assets (which may render your site broken), the worker skips such requests and _performs then as usual_.

We can get rid of such requests duplication if it's possible to short-circuit a request handling in the fetch event of the Service Worker. So that we don't return anything in case of a static asset, stating that such request should be performed as is (outside of the worker).

It seems that short circuiting within the fetch event handler function in the worker bypasses the request (it gets performed as-is) without it displaying in the XHR tab. I've issued a pull request with the fix, expect it in the nearest future.

Although I see a problem in Chrome dev tools, this is not the case for Firefox. It filters XHR requests just fine. Maybe it is something with dev tools?

Service Worker implementation and thus behavior may differ in different browsers. There's nothing we can or should do about this, unfortunately.

During the work on this I've found out a few details that I'd like to share.

Short answer

No, this won't be possible to do due to the Service Worker specification.

Long answer

The worker's "fetch" event triggers for any HTTP request from the client once the worker is active. The fetch event handler is a _synchronous function_, which may call event.respondWith() to respond with any Response, but it must call it synchronously:

self.addEventListener('fetch', function (event) {
  event.respondWith(anyPromise) // OK

  await somePromise()
  event.respondWith(...) // INVALID STATE
})

So there's the first behavior we learn:

  • event.respondWith() must be called synchronously in the "fetch" event handler.

To fetch data in this worker's event handler you can use the fetch() API. Since it's async, we can only use it _within_ the respondWith() Promise:

event.respondWith(async () => {
  return fetch('...').then(data => data.json())
})

Whenever you perform a fetch() call within the worker's scope it gets registered as the XHR in the Network tab. That is the behavior one cannot change. Thus, the next discovery:

  • fetch() calls in the worker's scope are always registered as XHR

Now let's analyze what happens to a request. Specifically, to a static asset request that shouldn't be mocked. Here's the journey it undergoes:

  1. Client loads its JavaScript code.
  2. The JS code registers the worker.
  3. The JS code performs an HTTP request.
  4. Request is caught by the "fetch" event of the worker.
  5. Worker signals to MSW to see if this request should be mocked (async action).
  6. If not, it performs request as is via event.respondWith(fetch(event.request)).

Since the step 5 is async, it must be nested in the event.respondWith() promise. If the request shouldn't be mocked, to perform an original request one needs to call fetch(event.request), which always produces an XHR request. This means that the requests that should be bypassed are performed as XHR and you can see them twice in your browser's Network tab.

There are much more nuances to the async worker behavior. For instance, there is a time window between steps 2 and 3 and if a request happens there it's _always_ bypassed, as that means "the worker is not ready to signal to MSW". _Those_ requests will not be listed as XHR, because the fetch event handler short circuits on those requests _outside_ of the event.respondWith() function call.

If this request duplication in the Network is confusing, this is how you can think of it:

  1. First (non XHR) request is what your app makes.
  2. Second (XHR) request is what the worker makes in order to fetch the original (bypassed) respond data and use it to respond to your first request.

@kettanaito thanks for your investigation and for a very detailed response! Do you have any idea, why it is possible to filter XHR requests in FF dev tools?

As I've mentioned, Service Worker implementation may differ in browsers. I suppose Firefox can understand that this XHR originates from the worker, and hides it in the list of all XHR. Chrome can understand that as well, but doesn't hide those requests. I support Chrome's behavior here, as the worker indeed performs an extra request to use for the actual response, thus it makes sense to list that extra request.

Really good explanation, it makes sense now. Thank you!

I'm closing the issue because there isn't much we can do on this behavior. The PR that slightly improves the requests bypassing will land in the next minor version. Thanks for raising this!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lukesmurray picture lukesmurray  路  3Comments

hauptrolle picture hauptrolle  路  4Comments

dashed picture dashed  路  3Comments

kettanaito picture kettanaito  路  3Comments

dashed picture dashed  路  4Comments