Skip to content

fix(ui): guard MessageCard._updateWidthLimit against unlaid RenderBox#2702

Merged
xsahil03x merged 1 commit into
GetStream:masterfrom
Mounix99:fix/message-card-hassize-guard
Jun 1, 2026
Merged

fix(ui): guard MessageCard._updateWidthLimit against unlaid RenderBox#2702
xsahil03x merged 1 commit into
GetStream:masterfrom
Mounix99:fix/message-card-hassize-guard

Conversation

@Mounix99
Copy link
Copy Markdown
Contributor

@Mounix99 Mounix99 commented Jun 1, 2026

Submit a pull request

Linear: FLU-

Github Issue: #

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

Bug

_MessageCardState._updateWidthLimit is scheduled via WidgetsBinding.instance.addPostFrameCallback from didChangeDependencies, and reads renderBox.size.width when it fires:

void _updateWidthLimit() {
  final attachmentContext = attachmentsKey.currentContext;
  final renderBox = attachmentContext?.findRenderObject() as RenderBox?;
  final attachmentsWidth = renderBox?.size.width;          // <-- throws
  ...
}

If the attachments subtree has been detached from the tree between scheduling and firing (e.g. the parent rebuilt the list, the chat was disposed, the attachment was deleted, the user navigated away), then per Flutter's contract RenderBox.size throws StateError: RenderBox was not laid out:

A RenderBox object must be laid out before any method that reads its size or position is called.

The throw is caught by FlutterError.onError, so the user sees a dropped frame rather than a crash. But it surfaces as a fatal error in crash reporters like Sentry. In a production Flutter app pinned to stream_chat_flutter: ^9.24.0, the two Sentry issues corresponding to this code path have generated over 1500 events across 1100+ unique users in a single 2-week release — the loudest non-network issue in the audit.

Sample symbolicated stack:

at _MessageCardState.didChangeDependencies.<T> (message_card.dart:155)
at _MessageCardState._updateWidthLimit (message_card.dart:138)
at RenderBox.size (box.dart:2304)
    (throw StateError('RenderBox was not laid out: $runtimeType#${shortHash(this)}'));

Fix

  1. Add a hasSize guard before reading renderBox.size — exactly what the framework docs recommend for the "read size after a frame" pattern.
  2. Move the existing mounted check from "before setState" to the top of the function. Previously, on an unmounted widget we still touched the (possibly unlaid) render object — only the setState was skipped. The earlier check is symmetrical and slightly cheaper.

Diff:

void _updateWidthLimit() {
  if (!mounted) return;

  final attachmentContext = attachmentsKey.currentContext;
  final renderBox = attachmentContext?.findRenderObject() as RenderBox?;
  // The attachments subtree may have been detached between scheduling this
  // post-frame callback and it firing. Reading [RenderBox.size] without
  // checking [RenderBox.hasSize] throws `RenderBox was not laid out`.
  if (renderBox == null || !renderBox.hasSize) return;
  final attachmentsWidth = renderBox.size.width;

  if (attachmentsWidth == 0) return;

  setState(() => widthLimit = attachmentsWidth);
}

Testing

This is a defensive guard against a framework-level race that fires intermittently on real user devices and is hard to reproduce locally. The guard preserves the existing happy-path behavior (when the render object is laid out, width is read and applied identically to before) and short-circuits cleanly when it's not.

Screenshots / Videos

Not applicable — this fix removes a race-condition error log, no visual change.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a crash when measuring attachment widths if attachments are removed before a post-frame update; width checks now avoid invalid reads and skip updates when not applicable.
  • Tests
    • Added widget tests verifying width measurement behavior, unmount safety, zero-width handling, and no-op when no attachments exist.
  • Documentation
    • Updated changelog with the fix.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68389b0d-e665-4559-bc92-fad339ed8c16

📥 Commits

Reviewing files that changed from the base of the PR and between 038045e and a52304a.

📒 Files selected for processing (3)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_widget/message_card.dart
  • packages/stream_chat_flutter/test/src/message_widget/message_card_test.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_widget/message_card.dart

📝 Walkthrough

Walkthrough

The pull request refactors the _updateWidthLimit method in the message card widget to safely compute attachment subtree width during post-frame updates. It adds a mounted check, validates the render box state before accessing dimensions, and only updates width limits when valid non-zero measurements are available.

Changes

Attachment Width Measurement Safety

Layer / File(s) Summary
Safe width computation in message attachments
packages/stream_chat_flutter/lib/src/message_widget/message_card.dart, packages/stream_chat_flutter/CHANGELOG.md
The _updateWidthLimit method adds an early mounted guard, fetches and validates the attachments render box with hasSize checks before reading size.width, returns when the measured width is zero, and updates widthLimit only with valid measurements via setState. The changelog adds a “Fixed” entry describing the hasSize guard and the avoided RenderBox was not laid out exception.
Tests for width measurement and guards
packages/stream_chat_flutter/test/src/message_widget/message_card_test.dart
Adds _FixedSizeAttachmentBuilder and widget tests that assert width constraint application, safe unmount-before-post-frame behavior, zero-width early return, and skipping measurement when no attachments are present.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Suggested reviewers

  • VelikovPetar
  • xsahil03x

Poem

🐰 A rabbit hops through render trees with care,
Guards against the unmounted and the bare,
Checks the box before it reads the size—
Safe width limits, wise and crystalline! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding safety guards to MessageCard._updateWidthLimit to prevent crashes when accessing an unlaid RenderBox.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.66%. Comparing base (7f0804d) to head (a52304a).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2702      +/-   ##
==========================================
+ Coverage   65.38%   65.66%   +0.27%     
==========================================
  Files         423      423              
  Lines       26665    26666       +1     
==========================================
+ Hits        17435    17509      +74     
+ Misses       9230     9157      -73     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mounix99 Mounix99 force-pushed the fix/message-card-hassize-guard branch from 4558737 to 038045e Compare June 1, 2026 15:23
@Mounix99 Mounix99 changed the title fix(message_widget): guard MessageCard._updateWidthLimit against unlaid RenderBox fix(ui): guard MessageCard._updateWidthLimit against unlaid RenderBox Jun 1, 2026
…id RenderBox

`_MessageCardState._updateWidthLimit` is scheduled via
`WidgetsBinding.instance.addPostFrameCallback` from `didChangeDependencies`,
and reads `renderBox.size.width` when it fires. If the attachments subtree
has been detached from the tree between scheduling and firing (e.g. the
parent removed the message because the list was rebuilt, the chat was
disposed, or the attachment was deleted), `RenderBox.size` throws
`StateError: RenderBox was not laid out` per the framework contract.

The throw is caught by `FlutterError.onError`, so users see a dropped frame
rather than a crash — but Sentry still records it as a fatal error. In one
production Flutter app the two corresponding Sentry issues have generated
1500+ events across 1100+ unique users in a single 2-week release. The error
appears under any view path that has chat messages with attachments (chat,
groupPost, splash, root).

Fix is a `hasSize` guard before reading `.size`, plus moving the existing
`mounted` check to the top of the function for symmetry (it was previously
only checked before `setState`, so we still touched the unmounted render
object first).

The render-box-was-not-laid-out error is the canonical signature of this
mistake in Flutter; the `hasSize` check is exactly what the framework docs
recommend for this case.
@Mounix99 Mounix99 force-pushed the fix/message-card-hassize-guard branch from 038045e to a52304a Compare June 1, 2026 15:44
@xsahil03x xsahil03x merged commit 03c8827 into GetStream:master Jun 1, 2026
22 checks passed
xsahil03x added a commit that referenced this pull request Jun 1, 2026
Resolves SPL conflicts in scrollable_positioned_list and positioned_list:
- Take master's preserve-pixels `_updateFirstVisibleItemIfNeeded` (PR #2703)
- Take master's empty-list `_centerSliverPadding` guard
- Take master's RTL `_axisDirection`-based `_updatePositions` branching
- Take master's `_resolveLeadingEndPaddingAdjust` `index != 0` check
- Drop orphaned `message_card_test.dart` (widget deleted in v10 design refresh);
  master's MessageCard fix from #2702 ported to StreamMessageContent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants