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

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


Pull request description:
perf: Avoid CORS preflight request by removing upload listener when not used
### New Pull Request Checklist
<!--
    Check every following box [x] before submitting your PR.
    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 creating this PR in reference to an [issue](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

### Issue Description
<!-- Add a brief description of the issue this PR solves. -->

Related issue: #1609
Closes #1609

Starting with v2.13.0, most Parse JS SDK operations began triggering the need for a preflight request. 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.

### Approach
<!-- Add a description of the approach in this PR. -->

Only set the progress listeners on the XHR when a progress callback is provided.

### TODOs before merging
<!--
    Add TODOs that need to be completed before merging this PR.
    Delete TODOs that do not apply to this PR.
-->

- [x] Add tests

Patch:
diff --git a/src/RESTController.js b/src/RESTController.js
index 4e06532..520358f 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 f2074c4..85524d8 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 866ead4..80d931c 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;