Skip to content

fix: Reject deleted flags and segments in flag overrides - #2187

Open
kinyoklion wants to merge 1 commit into
mainfrom
rlamb/overrides-node-reject-deleted
Open

kinyoklion wants to merge 1 commit into
mainfrom
rlamb/overrides-node-reject-deleted

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

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

Implements the "reject deleted entries" rule of launchdarkly/sdk-specs#288 (OVERRIDE Requirements 1.1.1.1, 1.2.8.1 and 1.3.6.2): the override layer never holds a flag or segment marked as deleted.

Describe the solution you've provided

Before:

  • The sink accepted a flag or segment with "deleted": true and served it as a live definition. A deleted flag was rejected only when it also lacked variations, and the message was "variations" must be an array, not about deletion. A full definition plus deleted, and any deleted segment, were stored and served.
  • The file override source loaded files that contained such entries.

After:

  • Sink and layer. A snapshot that contains a flag or segment with "deleted": true is rejected as a whole, from any source, including a custom one. The layer is unchanged and no change notifications are sent. The sink still throws to the source, now with flag "f" is marked as deleted; to remove its override, leave it out of the snapshot. The SDK also logs the rejection at Error level with the kind and the key, because a custom source may not report the error. The check runs over the whole snapshot before the shape checks, so a deleted placeholder is reported as deleted. A deleted field that is not a boolean is now a shape error ("deleted" must be a boolean). "deleted": false is an ordinary definition and still loads.
  • File override source. A file with such an entry fails the reload while the file is read, before the sink is called, the same way as a file that cannot be parsed. The previous overrides stay and the retry applies. The failure is logged the way other load failures are, with the key and the file: Unable to load flags: flag "f" is marked as deleted; remove the entry to remove its override [/path/overrides.json].
  • Docs. One sentence on the file override source option and one on LDOverrideSink.setOverrides. Every public override API keeps its experimental note.

The ordinary file data sources are unchanged on purpose: the FDv1 FileDataSource and the FDv2 file initializer still load deleted entries as they always have. Rejecting them there would be a breaking change, planned for a later major version. They share the filedata engine with the override source through an injected policy, so the check is only in the override code (overrideDocument.ts, OverrideLayer.ts, OverrideSink.ts, validateDefinition.ts). New tests pin the data source behavior.

Tests added:

  • OverrideLayer.test.ts: rejects a deleted flag placeholder, a full flag plus deleted, a deleted segment placeholder, and a full segment plus deleted, and keeps the previous contents each time. Reports the deleted item even when another definition is malformed. Does not modify the supplied definitions. Accepts deleted: false and null. Rejects a non-boolean deleted on a flag and on a segment.
  • OverrideSink.test.ts: a deleted flag, a full flag plus deleted, and a deleted segment are each rejected. The layer is unchanged, nothing is notified, and exactly one Error log names the kind and the key.
  • LDClientImpl.overrides.test.ts: a custom source's snapshot with a deleted flag is rejected and logged. The LaunchDarkly value and the earlier override stay in effect in evaluation and in allFlagsState.
  • FileOverrideSource.test.ts: a deleted flag entry and a deleted segment entry each fail the reload and keep the overrides. The log names the key and the file. The retry repeats at debug level and recovers once the entry is removed. A deleted entry in one of two files fails the initial load. deleted: false entries load into the layer.
  • overrideDocument.test.ts: rejects deleted flag and segment entries in JSON and YAML, and accepts deleted: false.
  • FileDataSource.test.ts and fileDataInitilizerFDv2.test.ts: pin that both data sources still load deleted entries with no error.

All new rejection tests fail against the main sources. The deleted: false tests and the data source pins pass on both.

How I ran them:

  • yarn workspace @launchdarkly/js-sdk-common build, then in packages/shared/sdk-server: yarn test (86 suites, 1608 passed, 12 skipped), yarn lint (no errors; the warnings are already on main), yarn build.
  • yarn oxfmt --check on the changed files.
  • In packages/sdk/server-node: npx jest --ci --runInBand __tests__/LDClientNode.fileOverrides.test.ts __tests__/LDClientNode.fileDataSource (11 passed).

Describe alternatives you've considered

  • Leaving the deleted entry out of the snapshot instead of rejecting the snapshot. The spec rejects the whole snapshot, so the layer is always exactly a snapshot the source supplied. That matches what the file source does with a file it cannot parse.
  • Logging every sink rejection, including shape errors. The file source already logs those, and it demotes repeated retries to debug, so the sink would log them twice and once per retry. The sink logs only the deleted rejection, which the spec requires and which the file source never passes on.
  • Checking in the shared filedata engine. That would change the ordinary file data sources.

Additional context

#2154 rewrites some of the same doc comments (LDDataSystemOptions.ts, FileOverrideSource.ts, overrideDocument.ts), so whichever of the two merges second will need a small doc-comment rebase. The code changes do not overlap.

Opened from termag


Note

Overview
Flag override snapshots that include a flag or segment with deleted: true are now rejected end-to-end: the override layer never stores them, OverrideSink logs an error and throws without applying the snapshot or notifying listeners, and the file override parser fails the reload with a message that names the file and key.

Removing an override is done by omitting the entry from the snapshot (or deleting the file), not by supplying a deleted marker. deleted: false and non-boolean deleted values are handled as ordinary definitions or shape validation errors respectively. FDv1/FDv2 file data sources still accept deleted entries unchanged (breaking-change avoidance); only the override path enforces the new rule. Public docs on LDOverrideSink.setOverrides and file override options describe this behavior.

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

The override layer never holds a deleted item (sdk-specs #288, Requirements
1.1.1.1, 1.2.8.1 and 1.3.6.2). A snapshot that contains a flag or segment
with "deleted": true is rejected as a whole: the layer is unchanged, no
change notifications are sent, the sink throws to the source, and the SDK
logs the rejection at Error level with the kind and the key. The check runs
before the shape checks, so a deleted placeholder is reported as deleted,
not as a flag without variations. A "deleted" field that is not a boolean
is a shape error. "deleted": false is an ordinary definition.

The file-based override source fails a file that contains such an entry
while it reads the file, the same way as a file that cannot be parsed: the
previous overrides stay, the failure is logged with the file and the key,
and the retry applies.

The ordinary file data sources are unchanged on purpose. Rejecting deleted
entries there would be a breaking change. New tests pin their behavior.
@kinyoklion

Copy link
Copy Markdown
Member Author

bugbot review

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 26346 bytes
Compressed size limit: 44000
Uncompressed size: 171286 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 34573 bytes
Compressed size limit: 35000
Uncompressed size: 123262 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 32497 bytes
Compressed size limit: 32500
Uncompressed size: 153345 bytes

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 617ec2c. Configure here.

This branch has not been deployed

No deployments
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