Skip to content

fix: emit status event before closing controller on unrecoverable stream error#324

Merged
tanderson-ld merged 1 commit into
launchdarkly:mainfrom
johnbonds:fix/streaming-status-event-after-close
Jul 10, 2026
Merged

fix: emit status event before closing controller on unrecoverable stream error#324
tanderson-ld merged 1 commit into
launchdarkly:mainfrom
johnbonds:fix/streaming-status-event-after-close

Conversation

@johnbonds

@johnbonds johnbonds commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

None filed — happy to open one if you prefer tracking it that way.

Describe the solution you've provided

In StreamingDataSource, the SSE subscription's onError handler handles an unrecoverable error by calling stop() — which closes _dataController — and then adds a StatusEvent to that now-closed controller:

stop();                                        // closes _dataController
_dataController.sink.add(StatusEvent(...));    // throws StateError

This throws Bad state: Cannot add event after closing, which surfaces as an uncaught error in the host application (we see this in production via Crashlytics — 44 users / 65 events in the last week on our app alone, reported at streaming_data_source.dart:203 on v1.9.0; the same code is present on main). It also means consumers never receive the final StatusEvent, since the controller is already closed when it's added.

The trigger in the field is an unrecoverable error from the event source client (e.g. Unable to make request), which we most often see when the app resumes from background and the connection can't be re-established.

This PR reorders the two statements so the StatusEvent is added before stop() closes the controller — matching the ordering PollingDataSource already uses (event added first, stop() after). A regression test is included; it fails against the previous ordering and passes with this change.

One thing I deliberately left unchanged for the maintainers to consider: this StatusEvent is emitted with shutdown: false even though the source is permanently shutting down (_permanentShutdown = true). Setting shutdown: true might be more accurate, but it changes downstream state handling, so I kept this fix minimal.

Describe alternatives you've considered

Guarding the add with if (!_dataController.isClosed) would prevent the crash but would still silently drop the final status event; reordering both fixes the crash and delivers the event.


Note

Low Risk
Small ordering fix in error shutdown with a targeted test; no auth or data-model changes.

Overview
Fixes a production crash when the SSE stream hits an unrecoverable error (e.g. after resume from background): onError used to call stop() first, which closes _dataController, then add a StatusEvent—that second step threw Bad state: Cannot add event after closing and consumers never saw the final error status.

The handler now emits the StatusEvent first, then calls stop(), aligned with how PollingDataSource orders shutdown. A regression test asserts the status update is delivered without throwing when the mock stream reports an unrecoverable error.

Reviewed by Cursor Bugbot for commit 1cec4b0. Bugbot is set up for automated code reviews on this repo. Configure here.

BEGIN_COMMIT_OVERRIDE
fix: emit status event before closing controller on unrecoverable stream error
END_COMMIT_OVERRIDE

…eam error

StreamingDataSource's onError handler called stop(), which closes
_dataController, and then added a StatusEvent to the closed controller,
throwing 'Bad state: Cannot add event after closing'. Emit the event
before stopping, matching PollingDataSource's ordering.
@johnbonds johnbonds requested a review from a team as a code owner July 10, 2026 20:02
@tanderson-ld

Copy link
Copy Markdown
Contributor

Thank you for bringing this to our attention and for the contribution @johnbonds, we will review this shortly.

@tanderson-ld tanderson-ld merged commit 2bc2ac7 into launchdarkly:main Jul 10, 2026
9 checks passed
tanderson-ld added a commit that referenced this pull request Jul 10, 2026
…325)

## Summary

- Add `workflow_dispatch:` to `.github/workflows/release-please.yml` so
the release-please workflow can be re-fired manually from the Actions UI
without pushing a commit to `main`.

## Motivation

When a commit lands on `main` whose message release-please can't parse
(e.g. run
[29123151878](https://github.com/launchdarkly/flutter-client-sdk/actions/runs/29123151878/job/86462658991),
where the squash-merge body of PR #324 tripped the conventional-commits
parser), the workflow completes without opening a release PR. Recovering
currently requires either editing the offending PR body (to add a
`BEGIN_COMMIT_OVERRIDE` block) and then pushing another commit to `main`
to retrigger, or landing a follow-up commit whose sole purpose is to
fire the workflow. Adding `workflow_dispatch` lets us edit the PR body
and click "Run workflow" from the Actions UI instead.

## Test plan

- [ ] After merge, the release-please workflow appears in the Actions UI
with a "Run workflow" button on the `main` branch and re-runs cleanly on
demand.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single trigger addition on an existing release automation workflow; no
change to release-please steps, permissions, or publishing behavior.
> 
> **Overview**
> Adds **`workflow_dispatch`** to **`release-please.yml`**, so
release-please can be started from the GitHub Actions UI in addition to
pushes to **`main`**.
> 
> This supports recovery when a **`main`** push does not produce a
release PR (for example, a squash-merge message that release-please
cannot parse). After fixing the PR body or related metadata, you can
re-run the workflow without pushing a no-op commit to **`main`**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
90555f6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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