Repository: parse-community/Parse-SDK-JS (JavaScript)

Issue:
Performance regression due to CORS preflight request
### New Issue Checklist
<!--
    Check every following box [x] before submitting your issue.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Platform!
-->

- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
- [x] I have searched through [existing issues](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).
- [x] I can reproduce the issue with the latest versions of [Parse Server](https://github.com/parse-community/parse-server/releases) and the [Parse JS SDK](https://github.com/parse-community/Parse-SDK-JS/releases). <!-- We don't investigate issues for outdated releases. -->

### Issue Description
<!-- What is the specific issue? -->

When connecting to a different origin in the browser environment, CORS requires a preflight request except for specific cases. This preflight request adds overhead and latency, especially for the first request.

Starting with v2.13.0, most Parse JS SDK operations began triggering the need for a preflight request when previously no preflight request was being made, which is a performance regression.

This is due to the addition of a progress listener on upload for all XHR requests with `RESTController`: https://github.com/parse-community/Parse-SDK-JS/pull/1133 . This listener triggers the need for a preflight request: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/upload#sect1

> Note: Attaching event listeners to this object prevents the request from being a "simple request" and will cause a preflight request to be issued if cross-origin; see CORS.

While this upload progress listener is useful in cases where a progress callback is being used, it should not be set when no progress callback is provided to avoid the performance issue.

### Steps to reproduce
<!-- How can someone else reproduce the issue? -->

- Set `Parse.serverURL` to a different origin
- Call a Cloud Function or perform any query

### Actual Outcome
<!-- What outcome, for example query result, did you get? -->

- A CORS preflight request is made by the browser with method `OPTIONS` before making the actual request

### Expected Outcome
<!-- What outcome, for example query result, did you expect? -->

- No CORS preflight request is made

###  Environment
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->

Server
- Parse Server version: 4.10.20
- Operating system: macOS
- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): Local

Database
- System (MongoDB or Postgres): MongoDB
- Database version: 4.4
- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): Local

Client
- Parse JS SDK version: 3.5.0

### Logs
<!-- Include relevant logs here. Turn on additional logging by configuring VERBOSE=1 in your environment. -->



Discussion:

### Thanks for opening this issue!

- 🚀 You can help us to fix this issue faster by opening a pull request with a failing test. See our [Contribution Guide](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md) for how to make a pull request, or read our [New Contributor's Guide](https://blog.parseplatform.org/learn/tutorial/community/nodejs/2021/02/14/How-to-start-contributing-to-Parse-Server.html) if this is your first time contributing.

<!-- parse-issue-bot-meta-tag-id {"suggestPr":true,"thanksFeature":false,"thanksPr":false} -->


🎉 This change has been released in version [4.0.0-alpha.3](https://github.com/parse-community/Parse-SDK-JS/releases/tag/4.0.0-alpha.3)
🎉 This change has been released in version [4.0.0-beta.1](https://github.com/parse-community/Parse-SDK-JS/releases/tag/4.0.0-beta.1)
🎉 This change has been released in version [4.0.0](https://github.com/parse-community/Parse-SDK-JS/releases/tag/4.0.0)



Patch:
diff --git a/src/RESTController.js b/src/RESTController.js
index 4e0653239..520358ff2 100644
--- a/src/RESTController.js
+++ b/src/RESTController.js
@@ -169,24 +169,24 @@ const RESTController = {
         headers[key] = customHeaders[key];
       }
 
-      function handleProgress(type, event) {
-        if (options && typeof options.progress === 'function') {
+      if (options && typeof options.progress === 'function') {
+        const handleProgress = function (type, event) {
           if (event.lengthComputable) {
             options.progress(event.loaded / event.total, event.loaded, event.total, { type });
           } else {
             options.progress(null, null, null, { type });
           }
-        }
-      }
-
-      xhr.onprogress = event => {
-        handleProgress('download', event);
-      };
+        };
 
-      if (xhr.upload) {
-        xhr.upload.onprogress = event => {
-          handleProgress('upload', event);
+        xhr.onprogress = event => {
+          handleProgress('download', event);
         };
+
+        if (xhr.upload) {
+          xhr.upload.onprogress = event => {
+            handleProgress('upload', event);
+          };
+        }
       }
 
       xhr.open(method, url, true);


Test patch:
diff --git a/src/__tests__/RESTController-test.js b/src/__tests__/RESTController-test.js
index f2074c47a..85524d85c 100644
--- a/src/__tests__/RESTController-test.js
+++ b/src/__tests__/RESTController-test.js
@@ -570,6 +570,20 @@ describe('RESTController', () => {
     );
   });
 
+  it('does not set upload progress listener when callback is not provided to avoid CORS pre-flight', () => {
+    const xhr = {
+      setRequestHeader: jest.fn(),
+      open: jest.fn(),
+      upload: jest.fn(),
+      send: jest.fn(),
+    };
+    RESTController._setXHR(function () {
+      return xhr;
+    });
+    RESTController.ajax('POST', 'users', {});
+    expect(xhr.upload.onprogress).toBeUndefined();
+  });
+
   it('does not upload progress when total is uncomputable', done => {
     const xhr = mockXHR([{ status: 200, response: { success: true } }], {
       progress: {
diff --git a/src/__tests__/test_helpers/mockXHR.js b/src/__tests__/test_helpers/mockXHR.js
index 866ead408..80d931cb8 100644
--- a/src/__tests__/test_helpers/mockXHR.js
+++ b/src/__tests__/test_helpers/mockXHR.js
@@ -35,8 +35,14 @@ function mockXHR(results, options = {}) {
       this.readyState = 4;
       attempts++;
       this.onreadystatechange();
-      this.onprogress(options.progress);
-      this.upload.onprogress(options.progress);
+
+      if (typeof this.onprogress === 'function') {
+        this.onprogress(options.progress);
+      }
+
+      if (typeof this.upload.onprogress === 'function') {
+        this.upload.onprogress(options.progress);
+      }
     },
   };
   return XHR;