fix(Channel): guard render-phase channel.getConfig() against disconnected channels - #3257
fix(Channel): guard render-phase channel.getConfig() against disconnected channels#3257oliverlaz wants to merge 2 commits into
Conversation
β¦cted channels ChannelInner evaluated channel.getConfig() as an eager useState argument, which throws "You can't use a channel after client.disconnect() was called" once the channel is disconnected (current user removed / channel deleted). The throw happened during render, tearing down the surrounding subtree. Add an internal getChannelConfig() guard, apply it at every render-phase call site, use a lazy initializer so it no longer re-runs on every render, and early-return from handleEvent for a disconnected channel. Fixing Channel alone is not enough: the crash relocates to AttachmentSelector once ChannelInner stops throwing and its subtree renders. Closes #3254
π WalkthroughWalkthroughThe PR adds a shared disconnected-channel configuration guard. Channel rendering, events, and pagination avoid unsafe access. Composer and read-state flows skip operations that require an active client. Regression tests cover these paths. ChangesDisconnected channel safety
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: π₯ 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 |
|
Size Change: +739 B (+0.08%) Total Size: 885 kB π¦ View Changed
βΉοΈ View Unchanged
|
There was a problem hiding this comment.
Actionable comments posted: 1
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/MessageComposer/MessageComposer.tsx`:
- Around line 98-105: Recheck messageComposer.channel.disconnected inside the
finally callback before calling messageComposer.clear(), while preserving the
existing early return before createDraft(). Add a test covering an unresolved
createDraft(), unmounting and disconnecting the channel, then resolving the
draft and verifying clear() is not called.
πͺ Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8dfb987f-5afb-479d-a989-5f3fb44ef7e7
π Files selected for processing (12)
src/components/Channel/Channel.tsxsrc/components/Channel/__tests__/Channel.test.tsxsrc/components/MessageComposer/AttachmentSelector/AttachmentSelector.tsxsrc/components/MessageComposer/MessageComposer.tsxsrc/components/MessageComposer/__tests__/AttachmentSelector.test.tsxsrc/components/MessageComposer/__tests__/MessageInput.test.tsxsrc/components/MessageComposer/hooks/__tests__/useMessageComposerCommands.test.tsxsrc/components/MessageComposer/hooks/useMessageComposerCommands.tssrc/components/MessageList/hooks/__tests__/useMarkRead.test.tsxsrc/components/MessageList/hooks/useMarkRead.tssrc/utils/__tests__/getChannelConfig.test.tssrc/utils/getChannelConfig.ts
| // A disconnected channel (current user removed / channel deleted) cannot | ||
| // accept a draft, and neither createDraft() nor clear() are safe to call: | ||
| // both reach channel.getConfig(), which throws "You can't use a channel | ||
| // after client.disconnect() was called". The composer is going away with | ||
| // the channel, so there is nothing left worth persisting or resetting. | ||
| if (messageComposer.channel.disconnected) return; | ||
|
|
||
| messageComposer.createDraft().finally(() => messageComposer.clear()); |
There was a problem hiding this comment.
π©Ί Stability & Availability | π Major | β‘ Quick win
Recheck disconnection before clear().
Line 103 only protects the start of cleanup. A WebSocket event can disconnect the channel while createDraft() is pending. The finally callback then calls messageComposer.clear(), which reaches channel.getConfig() after client.disconnect() and can throw.
Recheck messageComposer.channel.disconnected inside finally. Add a test that leaves createDraft() pending, unmounts, disconnects the channel, resolves the draft promise, and verifies that clear() is not called.
Proposed fix
- messageComposer.createDraft().finally(() => messageComposer.clear());
+ void messageComposer.createDraft().finally(() => {
+ if (!messageComposer.channel.disconnected) {
+ messageComposer.clear();
+ }
+ });π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // A disconnected channel (current user removed / channel deleted) cannot | |
| // accept a draft, and neither createDraft() nor clear() are safe to call: | |
| // both reach channel.getConfig(), which throws "You can't use a channel | |
| // after client.disconnect() was called". The composer is going away with | |
| // the channel, so there is nothing left worth persisting or resetting. | |
| if (messageComposer.channel.disconnected) return; | |
| messageComposer.createDraft().finally(() => messageComposer.clear()); | |
| // A disconnected channel (current user removed / channel deleted) cannot | |
| // accept a draft, and neither createDraft() nor clear() are safe to call: | |
| // both reach channel.getConfig(), which throws "You can't use a channel | |
| // after client.disconnect() was called". The composer is going away with | |
| // the channel, so there is nothing left worth persisting or resetting. | |
| if (messageComposer.channel.disconnected) return; | |
| void messageComposer.createDraft().finally(() => { | |
| if (!messageComposer.channel.disconnected) { | |
| messageComposer.clear(); | |
| } | |
| }); |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/MessageComposer/MessageComposer.tsx` around lines 98 - 105,
Recheck messageComposer.channel.disconnected inside the finally callback before
calling messageComposer.clear(), while preserving the existing early return
before createDraft(). Add a test covering an unresolved createDraft(),
unmounting and disconnecting the channel, then resolving the draft and verifying
clear() is not called.
Codecov Reportβ
All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3257 +/- ##
==========================================
- Coverage 85.21% 85.18% -0.03%
==========================================
Files 507 509 +2
Lines 15957 15971 +14
Branches 5029 5033 +4
==========================================
+ Hits 13597 13605 +8
- Misses 2360 2366 +6 β View full report in Codecov by Harness. π New features to boost your workflow:
|
loadMore already short-circuits on channel.disconnected, but loadMoreNewer checked only online.current / navigator.onLine / hasNext and went on to call channel.query(), which throws for a disconnected channel. The existing try/catch swallowed it into a console.warn plus a spurious setLoadingMoreNewer dispatch on every scroll-to-bottom.
π― Goal
Fixes: #3254
ChannelInnercalledchannel.getConfig()directly in the component body. That call throwsYou can't use a channel after client.disconnect() was calledonce the channel is disconnected β which happens when the current user is removed from a channel or the channel is deleted. The flag is flipped by an async WS event while<Channel>is still mounted, so the throw landed in the render phase and tore down the surrounding subtree.Same failure class as #2393 and #3248.
π Implementation details
Added an internal
getChannelConfig(channel)helper that returnsundefinedfor a disconnected channel instead of callinggetConfig(), and applied it everywhere the config was read during render or in an effect:Channel.tsxβ the reported crash. Now also a lazyuseStateinitializer, so the call no longer re-runs on every render.AttachmentSelector.tsxanduseMessageComposerCommands.tsuseMarkRead.tshandleEventinChannel.tsxalso early-returns for a disconnected channel, and the composer skips draft creation on unmount.loadMoreNewerpicked up thechannel.disconnectedguard thatloadMorealready had β without it, scrolling to the bottom of a disconnected channel still queried a dead channel on every attempt (caught by the existingtry/catch, so only log noise and a redundant dispatch).Fixing
Channelalone is not enough: the crash relocates toAttachmentSelectoronceChannelInnerstops throwing and its subtree starts rendering.undefinedis already part ofgetConfig()'s return type, so degradation is graceful β no read events, no commands, and the attachment selector renders nothing instead of crashing.9 tests added, each verified to fail against the unfixed code first.
π¨ UI Changes
None.