Treat the duplicate message id send error as a successful delivery - #6587
Conversation
…ivery The backend rejects a message send whose id already exists with a 400 input error (code 4). This happens when a send is retried after its response was lost, for example on a connection drop before the ack was processed. The message was in fact delivered, so marking it as FAILED_PERMANENTLY shows a delivered message as failed and invites a resend loop that can never succeed. Mark it as COMPLETED instead, in line with the iOS SDK behavior.
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
|
@CodeRabbit review |
✅ Action performedReview finished.
|
WalkthroughAdds duplicate-message error classification and routes matching send failures through successful completion in both offline database and state listeners, with tests covering duplicate and other validation errors. ChangesDuplicate message handling
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant SendMessageListener
participant ErrorClassifier
participant SendHandler
SendMessageListener->>ErrorClassifier: classify failed send error
ErrorClassifier-->>SendMessageListener: duplicate or non-duplicate
SendMessageListener->>SendHandler: use success or failure handling
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
SDK Size Comparison 📏
|
…ith-a-duplicate-lazycolumn-key-when-a
…ith-a-duplicate-lazycolumn-key-when-a
…ith-a-duplicate-lazycolumn-key-when-a
|
|
🚀 Available in v7.7.0 |



Goal
When a message send is retried after its response was lost (for example, the connection drops before the ack is processed), the backend rejects the retry with a 400 input error: code 4, "a message with ID x already exists". The message was in fact delivered by the first attempt, but the SDK marked it as
FAILED_PERMANENTLY. The user then sees a delivered message as failed, and a manual resend fails again with the same error, so the message can never leave the failed state. The iOS SDK already treats this rejection as a successful send.This is the SDK side of the fix for the message list crash found in the e2e nightly (
test_userAddsReactionWhileOffline): the mock server used to accept the duplicate send and store a second copy with the same id, and the channel query response then crashed the Compose message list with a duplicateLazyColumnkey. The mock server now mirrors the backend rejection (see GetStream/stream-chat-test-mock-server#51), which makes this SDK path reachable in e2e.Resolves AND-1320
Implementation
Error.isDuplicateMessageError()extension that detects the backend rejection:Error.NetworkErrorwith server error code 4 (ChatErrorCode.VALIDATION_ERROR) and a message containing "already exists". This is the same detection the iOS SDK uses inMessageRepository.SendMessageListenerStateandSendMessageListenerDatabasenow route this failure to their existing success handling, so the message is stored withSyncStatus.COMPLETEDinstead ofFAILED_PERMANENTLY.Testing
Unit tests: new cases in
SendMessageListenerStateTestandSendMessageListenerDatabaseTestcover the duplicate rejection (message marked as completed) and a different validation error (message still marked as failed permanently).Manual steps:
adb shell svc wifi disableandadb shell svc data disableright after tapping send). The message must stay in the pending state.E2e:
test_userAddsReactionWhileOfflineexercises this flow when the send races the offline switch, together with the mock server PR above. Verified locally against the updated mock server (test passes; the duplicate send returns 400 and the message list contains the message once).Summary by CodeRabbit
Bug Fixes
Tests