Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(xhr): invoke "onloadend" even if response was not returned from the interceptor #387

Merged
merged 12 commits into from
Jun 29, 2023

Conversation

hpohlmeyer
Copy link
Contributor

@hpohlmeyer hpohlmeyer commented Jun 28, 2023

The onloadend event handler is not being called if we do not send mocked response from a request interceptor:

import { XMLHttpRequestInterceptor } from "@mswjs/interceptors/XMLHttpRequest";
import { BatchInterceptor } from "@mswjs/interceptors";

const interceptor = new BatchInterceptor({
  name: "my-interceptor",
  interceptors: [new XMLHttpRequestInterceptor()],
});

interceptor.on("request", (req) => {
  console.log("new request", req);
})

interceptor.apply();

function handleResponse(event: any) {
  console.log(`[Response] Type: ${event.type}`);
  console.log(`[Response] Content: ${xhr.responseText}`);
}

const xhr = new XMLHttpRequest();
xhr.open("GET", <some-url>);

// This works as expected
xhr.addEventListener("loadend", handleResponse);

// This will never be called
xhr.onloadend = handleResponse;

xhr.send(null);

This is caused by onloadend not being located on XMLHttpRequest, but on XMLHttpRequestEventTarget which is part of its prototype chain (XMLHttpRequest <- XMLHttpRequestPrototype <- XMLHttpRequestEventTargetPrototype).

Because of that the property descriptor for onloadend is not found and we patch it on the XMLHttpRequest. When the browser calls the setter for onloadend, it does that on XMLHttpRequestEventTarget, which is why our proxy does not get notified.

This PR fixes the issue by walking the prototype chain, until it finds the property it is looking for. Redefining the property or looking for a property descriptor is done on the same level where the property is defined.

The vitest tests in xhr-event-handlers.test.ts do not complain about the issue, I am not sure in which environment they are running, but it is an issue in browsers. I have added a playwright test because of that.


As a sidenote: While looking for a fix, I found https://github.com/mswjs/interceptors/blob/v0.22.15/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts#L51-L64, which looks like it could be caused by a similar issue. I have removed the highlighted part and ran the tests. They run without errors, so if there is a test covering that part it might be fixed.

@hpohlmeyer hpohlmeyer closed this Jun 28, 2023
@hpohlmeyer
Copy link
Contributor Author

Closing this, because I did some more digging. I’ll open an issue instead to discuss how a proper fix could look like.

@hpohlmeyer
Copy link
Contributor Author

Reopening this with an updated description and fix, because I found the root cause of the issue and a proper fix for it.

@hpohlmeyer hpohlmeyer reopened this Jun 28, 2023
@kettanaito
Copy link
Member

Thanks for looking into this, @hpohlmeyer!

To double-check, have you encountered this issue while working with the latest version of this library? Since it's not the version most people install when installing msw today, I wanted to know whether we need to fix this as a backport to 0.17.x.

xhr.addEventListener('error', resolveDelayed)
xhr.addEventListener('load', resolveDelayed)
}),
xhr.send(null),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since xhr.send doesn't return a Promise, is there a reason to put these two in a single Promise.all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid adding the event listeners after the send call.

xhr.send(null)

await new Promise((resolve) => {
  const resolveDelayed = () => setTimeout(resolve, 1000)
  xhr.addEventListener('error', resolveDelayed)
  xhr.addEventListener('load', resolveDelayed)
})

seems to work fine though. Do think that’s fine or would you rather see a more specific version like:

xhr.send(null)

const finishedPromise = new Promise((resolve) => {
  const resolveDelayed = () => setTimeout(resolve, 1000)
  xhr.addEventListener('error', resolveDelayed)
  xhr.addEventListener('load', resolveDelayed)
})

xhr.send(null)
await finishedPromise

@hpohlmeyer
Copy link
Contributor Author

To double-check, have you encountered this issue while working with the latest version of this library? Since it's not the version most people install when installing msw today, I wanted to know whether we need to fix this as a backport to 0.17.x.

Yes, I am using @mswjs/interceptors on the latest version directly, not through msw.

Copy link
Member

@kettanaito kettanaito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fantastic change! Thank you, @hpohlmeyer. Looks ready to me!

@kettanaito kettanaito changed the title fix(xhr): fix onloadend not being called fix(xhr): invoke "onloadend" even if response was no returned from the interceptor Jun 29, 2023
@kettanaito kettanaito changed the title fix(xhr): invoke "onloadend" even if response was no returned from the interceptor fix(xhr): invoke "onloadend" even if response was not returned from the interceptor Jun 29, 2023
@kettanaito kettanaito merged commit 63efa9a into mswjs:main Jun 29, 2023
@kettanaito
Copy link
Member

Released: v0.22.16 🎉

This has been released in v0.22.16!

Make sure to always update to the latest version (npm i @mswjs/interceptors@latest) to get the newest features and bug fixes.


Predictable release automation by @ossjs/release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants