Skip to content

Commit e098ef1

Browse files
authored
fix: mark retryable duplicated messages as received (#2331)
### 🎯 Goal In some circumstances, the response about the delivery of a message may be aborted (timeout) or the web socket information about `message.new` might not arrive because of network conditions (offline, etc...). In this case, the SDK will prompt the user to re-send the message. Once the user retries, the backend will error with `SendMessage failed: Message with id XYZ already exists`. This error wasn't handled properly by the SDK and it led the user to re-attempt sending the same message over and over again in a loop. This loop could have been broken only by reloading the app. This PR aims to fix that. ### 🛠 Implementation details The SDK now recognizes this error, and once it happens, it immediately marks the message as `received`. This breaks the re-send loop for the user. Any UI inconsistencies (ordering of messages) related to the network glitches and optimistic rendering will be sorted out once the network connection restores and our SDK establishes a solid connection to our backend systems.
1 parent 80909c6 commit e098ef1

File tree

2 files changed

+70
-8
lines changed

2 files changed

+70
-8
lines changed

src/components/Channel/Channel.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import debounce from 'lodash.debounce';
1313
import defaultsDeep from 'lodash.defaultsdeep';
1414
import throttle from 'lodash.throttle';
1515
import {
16+
APIErrorResponse,
1617
ChannelAPIResponse,
1718
ChannelMemberResponse,
1819
ChannelQueryOptions,
1920
ChannelState,
21+
ErrorFromResponse,
2022
Event,
2123
EventAPIResponse,
2224
Message,
@@ -940,14 +942,34 @@ const ChannelInner = <
940942
} catch (error) {
941943
// error response isn't usable so needs to be stringified then parsed
942944
const stringError = JSON.stringify(error);
943-
const parsedError = stringError ? JSON.parse(stringError) : {};
944-
945-
updateMessage({
946-
...message,
947-
error: parsedError,
948-
errorStatusCode: (parsedError.status as number) || undefined,
949-
status: 'failed',
950-
});
945+
const parsedError = (stringError
946+
? JSON.parse(stringError)
947+
: {}) as ErrorFromResponse<APIErrorResponse>;
948+
949+
// Handle the case where the message already exists
950+
// (typically, when retrying to send a message).
951+
// If the message already exists, we can assume it was sent successfully,
952+
// so we update the message status to "received".
953+
// Right now, the only way to check this error is by checking
954+
// the combination of the error code and the error description,
955+
// since there is no special error code for duplicate messages.
956+
if (
957+
parsedError.code === 4 &&
958+
error instanceof Error &&
959+
error.message.includes('already exists')
960+
) {
961+
updateMessage({
962+
...message,
963+
status: 'received',
964+
});
965+
} else {
966+
updateMessage({
967+
...message,
968+
error: parsedError,
969+
errorStatusCode: parsedError.status || undefined,
970+
status: 'failed',
971+
});
972+
}
951973
}
952974
};
953975

src/components/Channel/__tests__/Channel.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,46 @@ describe('Channel', () => {
12611261
expect(await findByText(messageText)).toBeInTheDocument();
12621262
});
12631263

1264+
it('should mark message as received when the backend reports duplicated message id', async () => {
1265+
const { channel, chatClient } = await initClient();
1266+
// flag to prevent infinite loop
1267+
let hasSent = false;
1268+
const messageText = 'hello world';
1269+
const messageId = '123456';
1270+
1271+
let originalMessageStatus = null;
1272+
1273+
const { findByText } = await renderComponent(
1274+
{
1275+
channel,
1276+
chatClient,
1277+
children: <MockMessageList />,
1278+
},
1279+
({ sendMessage }) => {
1280+
jest.spyOn(channel, 'sendMessage').mockImplementation((message) => {
1281+
originalMessageStatus = message.status;
1282+
throw new chatClient.errorFromResponse({
1283+
data: {
1284+
code: 4,
1285+
message: `SendMessage failed with error: "a message with ID ${message.id} already exists"`,
1286+
},
1287+
status: 400,
1288+
});
1289+
});
1290+
if (!hasSent) {
1291+
sendMessage({ text: messageText }, { id: messageId, status: 'sending' });
1292+
}
1293+
hasSent = true;
1294+
},
1295+
);
1296+
expect(await findByText(messageText)).toBeInTheDocument();
1297+
expect(originalMessageStatus).toBe('sending');
1298+
1299+
const msg = channel.state.findMessage(messageId);
1300+
expect(msg).toBeDefined();
1301+
expect(msg.status).toBe('received');
1302+
});
1303+
12641304
it('should use the doSendMessageRequest prop to send messages if that is defined', async () => {
12651305
const { channel, chatClient } = await initClient();
12661306
// flag to prevent infinite loop

0 commit comments

Comments
 (0)