Skip to content

fix(webex-core): handle broken chain of promises#4873

Open
mkesavan13 wants to merge 3 commits intowebex:nextfrom
mkesavan13:people_get
Open

fix(webex-core): handle broken chain of promises#4873
mkesavan13 wants to merge 3 commits intowebex:nextfrom
mkesavan13:people_get

Conversation

@mkesavan13
Copy link
Copy Markdown
Contributor

@mkesavan13 mkesavan13 commented Apr 17, 2026

COMPLETES SPARK-747471

This pull request addresses

Issue #4588

Basically, the SDK was internally having an unhandled promise rejection that affected the flow of external apps.

by making the following changes

  • Cleaner promise chain handling on Batcher Requests
  • Ensuring to reject defers in case of handleHttpError where the request doesn't have a body. Right now the promise is rejected but the defer itself isn't. With this change, that case is handled
  • Not propagating caught rejects so that it doesn't again land in unhandledRejection

Restricted Cisco Only Vidcast for the fix: https://app.vidcast.io/share/69133bb4-84c1-4b08-a4cf-b0db6f05a233

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

Ran this snippet in Meetings Sample to not see the unhandledRejection handler being called in the following cases:

  1. API succeeded but the personId was incorrect
  2. API itself failed (Let's say the token is wrong)
// 1) Watch global unhandled rejections
window.addEventListener('unhandledrejection', (event) => {
  console.error('[UNHANDLED]', event.reason);
});

// 2) Trigger people.get() with guaranteed-invalid personId
(async () => {
  const uuid = crypto.randomUUID();
  const personId = "some_random_id";

  console.log('personId:', personId);
  try {
    await window.webex.people.get(personId);
  } catch (e) {
    console.error('[CAUGHT]', e);
  }
})();

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Cursor
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Make sure to have followed the contributing guidelines before submitting.

@mkesavan13 mkesavan13 requested review from a team as code owners April 17, 2026 11:28
@mkesavan13 mkesavan13 added the validated If the pull request is validated for automation. label Apr 17, 2026
Copy link
Copy Markdown
Contributor Author

@mkesavan13 mkesavan13 left a comment

Choose a reason for hiding this comment

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

Leaving notes of my findings with Cursor as co-pilot.

this.prepareItem(item)
.then((req) => {
defer.promise = defer.promise
.then(tap(() => this.deferreds.delete(idx)))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's the definition of tap:

function tap(fn) {
  return function (r) {
    return new _promise.default(function (resolve) {
      resolve(fn(r));
    }).then(function () {
      return r;
    }).catch(function () {
      return r;
    });
  };
}

It does nothing but return a curried function that returns a promise which executes and tries to resolve passed fn which in this case is () => this.deferreds.delete(idx)

If that's resolved, resolves with idx as value or when rejected, rejects with idx as value again.

Per the old flow, this was required to catch again to do the deletion.

Now, we have a cleaner code with promise.then receiving methods to resolve/reject directly

Comment on lines +122 to +130
return this.handleHttpError(reason).catch(() =>
Promise.all(
queue.map((item) =>
this.getDeferredForRequest(item).then((defer) => {
defer.reject(reason);
})
)
)
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Although handleHttpError is part of batcher and could have made this change directly there, the handleHttpError is overridden by downstream classes like avatar or metrics plugin. Therefore, this fix would get lost for such plugins.

If we add it in the executeQueue's catch block, the fix is propagated to all the downstream classes as executeQueue is nowhere overridden.

@aws-amplify-us-east-2
Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-4873.d3m3l2kee0btzx.amplifyapp.com

Copy link
Copy Markdown
Contributor

@akulakum akulakum left a comment

Choose a reason for hiding this comment

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

This looks like a solid fix for the unhandledRejection / unsettled-queue behavior described in the issue and demo: cleaning up the deferred promise handling, rejecting queued work when handleHttpError fails (including GET-style cases without a body), and avoiding a re-thrown rejection on a debounced path that nothing awaits. The unit tests for unhandledRejection and for failing queued deferreds are a good addition.

Before merge, could we confirm two things for maintainers and downstream consumers? First, with the final executeQueue catch now swallowing the rejection after logging, are we certain that every path that reaches that catch has already failed the relevant deferred promises, so we do not risk leaving a caller hanging? Second, for batcher subclasses that override handleHttpError, does the new fallback in executeQueue still guarantee that the full queue is rejected when appropriate?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

validated If the pull request is validated for automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants