fix(flutter): Guard script completion#3912
Conversation
Ignore repeated load and error events after the web script loader. Prevent SDK-originated StateError events when browsers redeliver terminal script events. Fixes #3906 Co-Authored-By: Codex <[email protected]>
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Features
FixesFlutter
Other
DependenciesDeps
Internal Changes
🤖 This preview updates automatically when you update the PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3912 +/- ##
==========================================
+ Coverage 87.56% 91.90% +4.33%
==========================================
Files 338 105 -233
Lines 12345 3828 -8517
==========================================
- Hits 10810 3518 -7292
+ Misses 1535 310 -1225
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a Flutter Web regression where a <script> element can emit more than one terminal event (load/error), previously causing Completer double-completion and surfacing StateError: Bad state: Future already completed as an SDK-originated fatal event.
Changes:
- Guard
Completercompletion in the web script DOM loader so subsequent terminal events are ignored. - Add browser regression tests covering
load → erroranderror → loadevent delivery. - Add
package:web/web.dartimport to support DOM event dispatching in tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/flutter/lib/src/web/script_loader/web_script_dom_api.dart | Adds isCompleted guards around onLoad/onError handlers to prevent double completion. |
| packages/flutter/test/web/sentry_script_loader_test.dart | Adds Chrome/browser tests ensuring second terminal event dispatch does not throw and future settlement is stable. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Keep package:web usage in the existing browser test utility. Use the same DOM abstraction as neighboring script loader tests. Co-Authored-By: Codex <[email protected]>
Name the browser helper after the event it dispatches without implying script ordering. Co-Authored-By: Codex <[email protected]>
Cancel both DOM event subscriptions after the script load future settles. Release their callbacks and captured state. Co-Authored-By: Codex <[email protected]>
Android Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9993fa9 | 389.44 ms | 390.85 ms | 1.41 ms |
| c741aa5 | 360.67 ms | 367.86 ms | 7.19 ms |
| cdf371b | 367.64 ms | 377.02 ms | 9.38 ms |
| ebccb5b | 363.79 ms | 371.10 ms | 7.31 ms |
| 6287dc4 | 390.30 ms | 399.12 ms | 8.82 ms |
| 51520fc | 351.89 ms | 349.79 ms | -2.10 ms |
| 3fe9e80 | 362.67 ms | 376.19 ms | 13.52 ms |
| d8f332d | 365.31 ms | 349.98 ms | -15.33 ms |
| 819c1e7 | 449.80 ms | 442.98 ms | -6.82 ms |
| 29e8ebe | 389.91 ms | 395.76 ms | 5.84 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9993fa9 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| c741aa5 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| cdf371b | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| ebccb5b | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 6287dc4 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 51520fc | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 3fe9e80 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| d8f332d | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 819c1e7 | 13.93 MiB | 15.00 MiB | 1.06 MiB |
| 29e8ebe | 13.93 MiB | 15.06 MiB | 1.13 MiB |
iOS Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| af96ef2 | 1260.79 ms | 1259.61 ms | -1.17 ms |
| b6c8720 | 1252.65 ms | 1266.61 ms | 13.96 ms |
| af4c7c1 | 1246.85 ms | 1255.35 ms | 8.50 ms |
| 8af916c | 1246.66 ms | 1240.79 ms | -5.87 ms |
| d0aa4b6 | 1268.23 ms | 1268.39 ms | 0.15 ms |
| 91629ec | 1257.91 ms | 1255.93 ms | -1.98 ms |
| c002f00 | 1252.47 ms | 1258.78 ms | 6.31 ms |
| a34fde0 | 1255.35 ms | 1254.31 ms | -1.03 ms |
| 3135a81 | 1258.11 ms | 1247.48 ms | -10.63 ms |
| d5a1225 | 1252.46 ms | 1254.66 ms | 2.20 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| af96ef2 | 5.53 MiB | 6.02 MiB | 501.31 KiB |
| b6c8720 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
| af4c7c1 | 5.83 MiB | 6.28 MiB | 460.73 KiB |
| 8af916c | 5.73 MiB | 6.18 MiB | 463.84 KiB |
| d0aa4b6 | 5.53 MiB | 6.02 MiB | 502.04 KiB |
| 91629ec | 5.83 MiB | 6.28 MiB | 459.63 KiB |
| c002f00 | 5.65 MiB | 6.09 MiB | 448.38 KiB |
| a34fde0 | 5.66 MiB | 6.10 MiB | 451.57 KiB |
| 3135a81 | 5.73 MiB | 6.17 MiB | 453.76 KiB |
| d5a1225 | 5.73 MiB | 6.17 MiB | 453.73 KiB |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ab59bbb. Configure here.
| completer.completeError('Failed to load $src'); | ||
| cancelSubscriptions(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Subscriptions leak on TT failure
Low Severity
Flagging per the Dart resource-disposal guidance in the review rules: load/error subscriptions are created before the Trusted Types try/catch, but cancelSubscriptions is not called when TrustedTypesException is thrown. The listener closures capture those subscriptions, so the unattached HTMLScriptElement can stay retained after policy creation fails.
Additional Locations (1)
Triggered by project rule: PR Review Guidelines for Cursor Bot (Root)
Reviewed by Cursor Bugbot for commit ab59bbb. Configure here.


📜 Description
The Flutter web script loader now settles its load future at most once and cancels both DOM event subscriptions afterward. Browser regression tests cover both load-then-error and error-then-load delivery.
💡 Motivation and Context
Browsers can deliver more than one terminal event for the same script element. The second event previously completed the same
Completeragain, surfacingStateError: Bad state: Future already completedas an SDK-originated fatal event. Keeping the subscriptions active after settlement also retained their callbacks and captured state for the lifetime of the script element.Closes #3906
💚 How did you test it?
Temporarily restored the original implementation and ran the browser suite. Both regression tests failed with
Bad state: Future already completed; with the fix restored, all browser tests passed.📝 Checklist
sendDefaultPiiis enabled