communication-ui-library icon indicating copy to clipboard operation
communication-ui-library copied to clipboard

ChatAdapter errors is not thrown to the error listener

Open DercilioFontes opened this issue 1 year ago • 5 comments

Describe the bug; what happened? I implemented the error handler for the ChatAdapter on the error listener, but the errors are not coming to it. I can see the error in the console logs.

export const errorCodes = [401, 403, 404];

...

const errorHandler = (e: ChatAdapterError) => {
  if (e.innerError.statusCode && errorCodes.includes(e.innerError.statusCode)) {
    chatAdapter?.dispose();
  }
};

...

chatAdapter.on('error', errorHandler);

...

<ChatComposite
    fluentTheme={customTheme}
    onFetchAvatarPersonaData={() => Promise.resolve(avatarPersonaData)}
    options={{ topic: false }}
    adapter={chatAdapter}
/>

Image: Screen Shot 2022-10-20 at 5 16 58 PM

What are the steps to reproduce the issue?

  1. Start a regular chat
  2. Leave one side, ending the thread in the server side
  3. Continue typing on the other side

In this case, we implemented to end the chat thread on the server side when one closes the browser, "unload" event. So, no event of removing the participant is done to be listened to on the other side. When the other side continues typing, it gets the error "Not Found." But the ChatAdapter doesn't throw it to the error listener.

What behavior did you expect? Errors are to be thrown to the ChatAdapter error listener.

If applicable, provide screenshots: Added above

In what environment did you see the issue? @azure/communication-chat: "^1.2.0", @azure/communication-common: "^2.1.0", @azure/communication-react: "^1.3.1",

  • OS & Device: MacOS 12.6
  • Browser: Google Chrome / Mozilla FireFox
  • Browser Version: Google Chrome Version 108.0.5355.0 (Official Build) dev (arm64) / Mozilla FireFox 105.0.3 (64-bit)

Is there any additional information? No

DercilioFontes avatar Oct 20 '22 21:10 DercilioFontes

Thanks for raising the issue @DercilioFontes! We have a afterCreate function that is an optional property to our useAzureCommunicationChatAdapter constructor for the adapter that is meant to be used to subscribe to these events. this little snippet is a example of how to use this function:

const errorHandler = (e: ChatAdapterError) => {
  if (e.innerError.statusCode && errorCodes.includes(e.innerError.statusCode)) {
    chatAdapter?.dispose();
  }
};

...
const chatAdapterArgs: AzureCommunicationChatAdapterArgs = {
  threadId: ...,
  userId: ...,
  token: ...,
  displayName: ...,
  endpoint: ...
};
...

const afterCreate = useCallBack(
  async (adapter: ChatAdapter): Promise<ChatAdapter> => {
    adapter.on('error', errorHandler);
  }
)


const adapter = useAzureCommunicationsChatAdapter(chatAdapterArgs, afterCreate);

Please let me know if you have any questions about this implementation!

dmceachernmsft avatar Oct 20 '22 22:10 dmceachernmsft

@dmceachernmsft, I tried to use your suggestion, but it will require changes in my code from how it is implemented now. I'll try when I have more time. Giving more info about how I implemented it, it follows:

I have one useEffect to create the adapter.

const [chatAdapter, setChatAdapter] = useState<ChatAdapter>();

...

  // Reserve this useEffect for only creating the ChatAdapter
  // Any dependency passed here that is updated will trigger useEffect call again and recreate the ChatAdapter losing the initial messages
  useEffect(() => {
    (async () => {
      if (chatUserConn?.threadId) {
        const adapter = await createChatAdapter(
          chatUserConn.endpointUrl,
          chatUserConn.communicationUserId,
          chatUserConn.displayName,
          chatUserConn.token,
          chatUserConn.threadId
        );

        setChatAdapter(adapter);
      }
    })();
  }, [
    chatUserConn?.communicationUserId,
    chatUserConn?.displayName,
    chatUserConn?.endpointUrl,
    chatUserConn?.threadId,
    chatUserConn?.token,
  ]);


// createChatAdapter imported from another file
export const createChatAdapter = async (
  endpointUrl: string,
  communicationUserId: string,
  displayName: string,
  token: string,
  threadId: string
) => {
  try {
    return await createAzureCommunicationChatAdapter({
      endpoint: endpointUrl,
      userId: fromFlatCommunicationIdentifier(communicationUserId) as CommunicationUserIdentifier,
      displayName: displayName,
      credential: createAutoRefreshingCredential(communicationUserId, token),
      threadId: threadId,
    });
  } catch (error) {
    ...
    return undefined;
  }
};

export const createAutoRefreshingCredential = (userId: string, token: string) => {
  const options: CommunicationTokenRefreshOptions = {
    token: token,
    tokenRefresher: refreshTokenAsync(userId),
    refreshProactively: true,
  };
  return new AzureCommunicationTokenCredential(options);
};

And I have another one to add the listeners.

// Listeners
  useEffect(() => {
    const messageReceivedHandler: MessageReceivedListener = (listener) => {
    ...
    };
    const messageSentHandler: MessageReceivedListener = (listener) => {
      ...
    };
    const participantsRemovedHandler: ParticipantsRemovedListener = (listener) => {
    ...
    };
    const endChatOnError = () => {
      chatAdapter?.dispose();
    };
    const errorHandler = (e: ChatAdapterError) => {
      if (e.innerError.statusCode && errorCodes.includes(e.innerError.statusCode)) {
        ...
      }
    };
    const windowErrorHandler = (e: PromiseRejectionEvent) => {
      if (errorCodes.includes(e.reason.statusCode)) {
       ...
      }
    };

    if (chatAdapter) {
      chatAdapter.on('messageReceived', messageReceivedHandler);
      chatAdapter.on('messageSent', messageSentHandler);
      chatAdapter.on('participantsRemoved', participantsRemovedHandler);
      chatAdapter.on('error', errorHandler);
      window.addEventListener('unhandledrejection', windowErrorHandler);
    }

    return () => {
      if (chatAdapter) {
        chatAdapter.off('messageReceived', messageReceivedHandler);
        chatAdapter.off('messageSent', messageSentHandler);
        chatAdapter.off('participantsRemoved', participantsRemovedHandler);
        chatAdapter.off('error', errorHandler);
        window.removeEventListener('unhandledrejection', windowErrorHandler);
      }
    };
  }, [chatAdapter]);

And you can see now that I added a 'unhandledrejection' event listener to handle the ChatAdapter errors. It is working fine.

DercilioFontes avatar Oct 21 '22 01:10 DercilioFontes

@DercilioFontes glad you were able to find a workaround here! seems like there is a issue inside our chat adapter if we have separate errors sneaking through! Could I get a little more info to point our search on this one?

  • When do these errors typically show up? Adapter creation? when using the composite to send and receive messages?
  • Which version of communication-react are you using? Thanks again for raising the issue for us!

dmceachernmsft avatar Oct 21 '22 16:10 dmceachernmsft

The version is 1.3.1

The errors happen when the thread is ended in the Communication Service, and one is typing and sending messages. The requests to the Service are returning 404 (Not Found, because the thread is not live anymore), and the error is not coming to the ChatAdapter error listener.

DercilioFontes avatar Oct 21 '22 17:10 DercilioFontes

Excellent. Thank you for the information! will update when we know more and when a fix is coming!

dmceachernmsft avatar Oct 21 '22 17:10 dmceachernmsft

Hi @DercilioFontes seems the issue was actually in our chat-stateful-client. we have a fix up for review and will have a release with it sometime soon!

dmceachernmsft avatar Oct 28 '22 23:10 dmceachernmsft

@dmceachernmsft, great! Thanks

DercilioFontes avatar Oct 28 '22 23:10 DercilioFontes