Skip to content

fix: handle duplicate id error - #1907

Open
szuperaz wants to merge 7 commits into
masterfrom
handle-duplicate-id-error
Open

szuperaz wants to merge 7 commits into
masterfrom
handle-duplicate-id-error

Conversation

@szuperaz

@szuperaz szuperaz commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

https://linear.app/stream/issue/REACT-1111/offline-queue-resent-messages-never-clear-the-failed-local-copy

Context

Backend team found (ticket) that the default JS request timeout is the same as the default backend write timeout: 3secs. This is what can happen:

  • Since stream-chat-js starts the timer when the request is sent, its 3sec timeout will finish earlier than backend's which lead to a client-side timeout rejection, but under the hood backend will the message to database.
  • On client-side if the message.new event arrives AFTER the client-side timeout rejection, all is good, the message will move to received state
  • However, if the rejection comes BEFORE message.new the SDKs' own error handler will flip the state back to failed
  • In this case the user will most likely resend the message, and get a message id already exists error from backend. Android and iOS SDKs already handles this by returning the local message and swallowing the error. We were asked by the backend team to handle it as well. React SDK already handles this, but RN didn't.
  • The same solution is ported to stream-chat-js: if sendMessage encounters the specific "id already exists" error, we swallow it and instead return a fake backend response from the local message (we strip all local-only fields/transforms)

How to reproduce

On web I'm able to reproduce it with using Chrome's network throttling (3g) and sending multiple messages in close proximity in time. (On RN Claude just set up a proxy)

RN + offline mode

In RN when offline mode is enabled, this bug can happen more frequently because sendMessage (and all offline-ready API methods) follow this logic:

  • Execute API call via offlineDb.queueTask
  • If that fails, regardless of the error, send without offline DB as a fallback

I assume this is some fallback mechanism to shield us from offline DB errors, but this means that:

  • Offline DB execute task fails with client-side timeout rejection
  • The fallback send immediately resends the message, and there is a good chance that the rejection from the fallback arrives later than message.new, so we end up in the failed state

After the fix, the fallback send will resolve with the local message. However, since at that point the offline DB already added a pending task, the fix also removes this pending task (v10 doesn't have this logic for removing the pending task, see the replay logic in that case in the next paragraph).

Despite removing the pending resend task in the use-case detailed above, it's still possible that RN ends up with a resend task and gets a "duplicate id error" when it replays message send: from what I was told by Claude RN will replay offline tasks without waiting for the offline DB to be loaded into JS state. And JS's sendMessage will only swallow the "duplicate id error" if that message is stored locally. Worst-case scenario here is the user clicks resend, and the new "duplicate id" handler flips the message to received state. (on v10 the flip to received state happens automatically as long as the failed message is part of the state we fetch from backend on reconnect/next app start)

Remaining issues

  • If we have a failed message not due to client-side timeout, but because of a genuine loss of connection, and regain connection, then do a state recovery: JS won't reconcile the message from server and the local message into one. We'll have two message with same id in local state: one with failed state, one with received. Verified by Claude with a unit test. Solved in v10
  • When an SDK displays a thread from thread instance, instead of channel.state threads failed thread reply state is never updated from HTTP response, this is true for any kind of failure. Solved in v10

A note about the connected GH ticket

The backend team automatically connected the #1838 ticket. It's not really clear how the bug in that ticket works (and the customer didn't answer to Ivan's questions). There is a chance that maybe they hit the issue of "losing connection before receiving backend reply" since they mention going offline? But even if their issue is the offline path, that shouldn't be a permanent failed state anymore after the fix. So it is safe to close that ticket.

@szuperaz szuperaz changed the title Handle duplicate id error fix: handle duplicate id error Oct 9, 2026
@szuperaz
szuperaz marked this pull request as ready for review October 9, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant