Skip to content

fix: add retry logic to loadThreadMessages to prevent missing thread …#7016

Open
deepak0x wants to merge 4 commits intoRocketChat:developfrom
deepak0x:fix/thread-preview-missing-messages
Open

fix: add retry logic to loadThreadMessages to prevent missing thread …#7016
deepak0x wants to merge 4 commits intoRocketChat:developfrom
deepak0x:fix/thread-preview-missing-messages

Conversation

@deepak0x
Copy link
Contributor

@deepak0x deepak0x commented Feb 27, 2026

When loadThreadMessages fails (due to network error, timeout, or server error), the load() function silently catches the error and returns an empty array [].

This causes the thread view in the native app to display only a subset of messages.

Specifically, only messages that were:

  • Already cached from channel history (messages with tshow: true / "Also send to channel")
  • Received in real-time while the user was actively online in the room

The web client does not have this issue because it uses a different fetching strategy with proper error handling.


Cause

The load() function contains a silent catch block:

  • Errors from loadThreadMessages are swallowed
  • An empty array [] is returned
  • No retry is attempted
  • No error is propagated to the caller

This makes the failure invisible and results in incomplete thread data.


Closes #6311

Proposed changes

This PR improves reliability by:

  1. Adding retry logic

    • 3 retry attempts
    • Backoff delays: 500ms → 1000ms → 1500ms
    • Handles transient network failures
  2. Propagating errors after retries fail

    • Instead of returning [], the error is thrown
    • Allows the caller (RoomView.init()) to trigger its own retry mechanism
    • Creates a two-layer retry system
  3. Removing silent error swallowing

    • Eliminates the catch { return []; }
    • Prevents hidden data loss

How to Test / Reproduce

  1. Go to Settings > Preferences > Also send thread message to channel behavior.

  2. Choose "Selected by default".

  3. Create a thread message.

  4. Reply in that thread with "Also send to channel" enabled.

  5. Reply in that thread with "Also send to channel" disabled.

  6. Repeat with other preference settings:

    • "Selected for first reply, unselected for following replies"
    • "Unselected by default"
  7. Compare the thread view in the native app vs the web version.

Before Fix

  • Some thread replies are missing in the native app.
  • Especially replies sent without "Also send to channel".
  • Happens when the initial getThreadMessages call fails.

After Fix

  • All thread messages are shown reliably.
  • Retry logic handles transient failures.
  • No silent data loss.

Screenshots

N/A — No UI changes. This is a data-fetching reliability improvement.


Type of Change

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • Bug Fixes
    • Improved thread message loading reliability by adding automatic retries with incremental backoff (up to 4 attempts), reducing transient failures and resulting in more consistent message delivery and fewer empty results for users.

…messages

When loadThreadMessages fails (network error, timeout), it silently
returns [] causing the thread view to show only a subset of messages.
The web client shows all messages correctly for the same thread.

This adds retry logic (3 attempts with increasing backoff) and
propagates errors to the caller (RoomView.init) which has its own
retry mechanism, ensuring thread messages are reliably loaded.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbce023 and 5e9f927.

📒 Files selected for processing (1)
  • app/lib/methods/loadThreadMessages.ts
📜 Recent review details
🔇 Additional comments (1)
app/lib/methods/loadThreadMessages.ts (1)

14-32: LGTM! Retry logic implementation is correct.

The retry implementation properly addresses the issue:

  • 4 total attempts (1 initial + 3 retries) with linear backoff of 500ms, 1000ms, 1500ms
  • Errors are propagated after all retries are exhausted, allowing callers to handle failures
  • ESLint directives correctly scope the no-await-in-loop exception to the retry loop
  • No dead code after the loop since all paths either return or throw

This fixes the silent failure that was causing missing thread messages.


Walkthrough

Replaces a single call to fetch thread messages with a retry loop (MAX_ATTEMPTS = 4) that awaits each attempt, returns parsed EJSON on success, returns [] immediately for falsy results, waits incremental backoff between retries, and rethrows after the final failed attempt. No exported signatures changed.

Changes

Cohort / File(s) Summary
Thread Message Fetching with Retry Logic
app/lib/methods/loadThreadMessages.ts
Replaced single sdk.methodCallWrapper('getThreadMessages', { tmid }) call with a for-loop retry (MAX_ATTEMPTS = 4). Each attempt awaits the SDK call; on falsy result returns [] immediately; on truthy result returns EJSON.fromJSONValue(result). On failure, sleeps attempt * 500 ms before retrying and rethrows after the final attempt. Removed previous outer try/catch and unconditional empty-array fallback. ESLint in-loop-await directive retained.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately describes the main change: adding retry logic to loadThreadMessages. It's specific, concise, and directly reflects the PR's primary objective.
Linked Issues check ✅ Passed Changes directly address #6311 by implementing retry logic with backoff delays and error propagation to fix missing thread messages in native client.
Out of Scope Changes check ✅ Passed All changes are scoped to loadThreadMessages.ts and directly support the retry logic implementation required to fix the missing thread messages issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/lib/methods/loadThreadMessages.ts`:
- Around line 14-31: Remove the unreachable trailing "return []" after the retry
loop in loadThreadMessages; the for-loop either returns a value on success or
re-throws on the final failed attempt, so delete the extraneous return to avoid
dead code (references: loadThreadMessages, MAX_RETRIES, the for (let attempt...)
retry loop and its try/catch).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 566e100 and a4c236b.

📒 Files selected for processing (1)
  • app/lib/methods/loadThreadMessages.ts
📜 Review details
🧰 Additional context used
🪛 ESLint
app/lib/methods/loadThreadMessages.ts

[error] 18-18: Unexpected await inside a loop.

(no-await-in-loop)


[error] 25-25: Unexpected await inside a loop.

(no-await-in-loop)

🔇 Additional comments (1)
app/lib/methods/loadThreadMessages.ts (1)

34-95: LGTM on the error propagation integration.

The outer function correctly catches errors thrown from load() after retry exhaustion and propagates them via reject(e). This enables the caller (RoomView.init) to handle the failure appropriately, which aligns with the PR objective.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/lib/methods/loadThreadMessages.ts`:
- Around line 14-25: The loop currently sets MAX_RETRIES = 3 which yields 3
total attempts; to get the required "initial try + 3 retries" with
500/1000/1500ms backoff, update MAX_RETRIES to 4 in loadThreadMessages.ts (so
the for loop for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) will run 4
times) and keep the existing delay logic (await new Promise(resolve =>
setTimeout(resolve, attempt * 500))) so retries occur with 500ms, 1000ms, 1500ms
delays after the initial failure of sdk.methodCallWrapper('getThreadMessages', {
tmid }).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4c236b and 9acd9d4.

📒 Files selected for processing (1)
  • app/lib/methods/loadThreadMessages.ts
📜 Review details
🧰 Additional context used
🪛 ESLint
app/lib/methods/loadThreadMessages.ts

[error] 18-18: Unexpected await inside a loop.

(no-await-in-loop)


[error] 25-25: Unexpected await inside a loop.

(no-await-in-loop)

🔇 Additional comments (1)
app/lib/methods/loadThreadMessages.ts (1)

18-22: Verify whether methodCallWrapper throws on transient failures or returns null/undefined.

The code exhibits an asymmetry: exceptions in the DDP/REST call trigger retries (lines 23–28), but if methodCallWrapper returns a falsy value (null/undefined), the function returns [] immediately at line 20 without retrying. This is problematic only if methodCallWrapper uses null/undefined to signal transient failures rather than throwing an exception.

The current code structure treats falsy as a valid "no messages" response, and the outer function also handles empty arrays gracefully (line 37). However, without confirmation of methodCallWrapper's actual contract—specifically whether it throws on transient failures or returns falsy—it cannot be determined if this is a real failure-handling gap or intentional design. Check the SDK or server-side implementation to clarify whether transient failures result in exceptions (which would be caught and retried) or falsy returns (which currently bypass retries).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/lib/methods/loadThreadMessages.ts (1)

14-32: Retry implementation looks good.

The retry logic is well-structured with appropriate exponential backoff. The ESLint rule disable is justified here since sequential awaits are required for retry semantics.

Minor observation: The function lacks an explicit return type. Since it can return [], EJSON.fromJSONValue(result) (which is any), or throw, consider adding a return type annotation for clarity and type safety.

,

💡 Optional: Add explicit return type
-async function load({ tmid }: { tmid: string }) {
+async function load({ tmid }: { tmid: string }): Promise<TThreadMessageModel[]> {

This would require ensuring EJSON.fromJSONValue(result) returns the expected type, potentially with a type assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/methods/loadThreadMessages.ts` around lines 14 - 32, The function
loadThreadMessages should declare an explicit return type; add a return type
annotation to its signature (e.g., Promise<MessageType[]>) and ensure the call
to EJSON.fromJSONValue(result) is typed or asserted to that same type (or
map/validate the result to MessageType[]), so the function consistently returns
the annotated array type or throws; update any imports/types (e.g., MessageType)
and adjust the sdk.methodCallWrapper generic/typing if needed to propagate the
correct type to EJSON.fromJSONValue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/lib/methods/loadThreadMessages.ts`:
- Around line 14-32: The function loadThreadMessages should declare an explicit
return type; add a return type annotation to its signature (e.g.,
Promise<MessageType[]>) and ensure the call to EJSON.fromJSONValue(result) is
typed or asserted to that same type (or map/validate the result to
MessageType[]), so the function consistently returns the annotated array type or
throws; update any imports/types (e.g., MessageType) and adjust the
sdk.methodCallWrapper generic/typing if needed to propagate the correct type to
EJSON.fromJSONValue.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9acd9d4 and bbce023.

📒 Files selected for processing (1)
  • app/lib/methods/loadThreadMessages.ts
📜 Review details
🔇 Additional comments (1)
app/lib/methods/loadThreadMessages.ts (1)

35-38: Error propagation is now correctly handled.

The try/catch block properly catches errors thrown by load() after retry exhaustion and rejects the promise, allowing callers (e.g., RoomView.init) to handle failures. This addresses the PR objective of preventing silent data loss.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Incomplete Thread Message Previews in Rocket.Chat Native (Compared to Web)

1 participant