From 2c7f550eaed3312bc0550db9f4402524074874ab Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:59:21 +0000 Subject: [PATCH] fix: Ignore FDv2 objects with unrecognized kinds put-object and delete-object events whose kind is not flag or segment are now dropped by the streaming and polling parsers before the object is decoded. Previously they were forwarded to the store, which filed any non-flag kind under segments and could overwrite or delete a real segment with the same key. --- lib/ldclient-rb/impl/data_system/polling.rb | 4 ++ .../impl/data_system/protocolv2.rb | 21 ++++++ lib/ldclient-rb/impl/data_system/streaming.rb | 14 ++-- .../polling_payload_parsing_spec.rb | 22 +++++++ .../streaming_synchronizer_spec.rb | 66 +++++++++++++++++++ 5 files changed, 123 insertions(+), 4 deletions(-) diff --git a/lib/ldclient-rb/impl/data_system/polling.rb b/lib/ldclient-rb/impl/data_system/polling.rb index 8c5d1266..629e8e9d 100644 --- a/lib/ldclient-rb/impl/data_system/polling.rb +++ b/lib/ldclient-rb/impl/data_system/polling.rb @@ -497,6 +497,8 @@ def self.polling_payload_to_changeset(data) builder.start(server_intent.payload.code) when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT + next if LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(event[:data]) + begin put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(event[:data]) rescue ArgumentError => e @@ -506,6 +508,8 @@ def self.polling_payload_to_changeset(data) builder.add_put(put.kind, put.key, put.version, put.object) when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT + next if LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(event[:data]) + begin delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(event[:data]) rescue ArgumentError => e diff --git a/lib/ldclient-rb/impl/data_system/protocolv2.rb b/lib/ldclient-rb/impl/data_system/protocolv2.rb index ef6b5ad3..da1cfd02 100644 --- a/lib/ldclient-rb/impl/data_system/protocolv2.rb +++ b/lib/ldclient-rb/impl/data_system/protocolv2.rb @@ -1,4 +1,5 @@ require 'json' +require 'ldclient-rb/interfaces/data_system' module LaunchDarkly module Impl @@ -9,6 +10,26 @@ module ProtocolV2 # LaunchDarkly data system version 2 (FDv2). # + SUPPORTED_KINDS = [ + LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, + LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, + ].freeze + + # + # Returns true if put-object or delete-object data names a kind this SDK + # does not support. Such objects are ignored so that payloads carrying + # newer kinds can still be applied. + # + # @param data [Hash] The hash representation of the event data + # @return [Boolean] + # + def self.unsupported_kind?(data) + return false unless data.is_a?(Hash) + + kind = data[:kind] + !kind.nil? && !SUPPORTED_KINDS.include?(kind) + end + # # DeleteObject specifies the deletion of a particular object. # diff --git a/lib/ldclient-rb/impl/data_system/streaming.rb b/lib/ldclient-rb/impl/data_system/streaming.rb index ae37bcae..532fef21 100644 --- a/lib/ldclient-rb/impl/data_system/streaming.rb +++ b/lib/ldclient-rb/impl/data_system/streaming.rb @@ -246,13 +246,19 @@ def stop nil when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT - put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(JSON.parse(message.data, symbolize_names: true)) - change_set_builder.add_put(put.kind, put.key, put.version, put.object) + data = JSON.parse(message.data, symbolize_names: true) + unless LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(data) + put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(data) + change_set_builder.add_put(put.kind, put.key, put.version, put.object) + end nil when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT - delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(JSON.parse(message.data, symbolize_names: true)) - change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version) + data = JSON.parse(message.data, symbolize_names: true) + unless LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(data) + delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(data) + change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version) + end nil when LaunchDarkly::Interfaces::DataSystem::EventName::GOODBYE diff --git a/spec/impl/data_system/polling_payload_parsing_spec.rb b/spec/impl/data_system/polling_payload_parsing_spec.rb index 877ad5ce..e590285a 100644 --- a/spec/impl/data_system/polling_payload_parsing_spec.rb +++ b/spec/impl/data_system/polling_payload_parsing_spec.rb @@ -100,6 +100,28 @@ module DataSystem expect(result.value.selector.version).to eq(461) end + it "ignores put and delete objects of unknown kinds" do + payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sampleflag","kind":"flag","version":461,"object":{"key":"sampleflag","version":112}}},{"event": "put-object","data": {"key":"sampleskill","kind":"some-future-kind","version":461,"object":{"foo":1}}},{"event": "delete-object","data": {"key":"sampleskill","kind":"some-future-kind","version":461}},{"event": "delete-object","data": {"key":"samplesegment","kind":"segment","version":461}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength + + # Parse a payload that mixes known and unknown object kinds. + result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) + + # The payload completes with only the known objects. + expect(result.success?).to eq(true) + expect(result.value.intent_code).to eq(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL) + expect(result.value.changes.length).to eq(2) + + expect(result.value.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) + expect(result.value.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) + expect(result.value.changes[0].key).to eq(:sampleflag) + + expect(result.value.changes[1].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) + expect(result.value.changes[1].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT) + expect(result.value.changes[1].key).to eq(:samplesegment) + + expect(result.value.selector.state).to eq("(p:5A46PZ79FQ9D08YYKT79DECDNV:461)") + end + it "handles invalid put object" do payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true)) diff --git a/spec/impl/data_system/streaming_synchronizer_spec.rb b/spec/impl/data_system/streaming_synchronizer_spec.rb index 513c5cf7..860c5029 100644 --- a/spec/impl/data_system/streaming_synchronizer_spec.rb +++ b/spec/impl/data_system/streaming_synchronizer_spec.rb @@ -215,6 +215,72 @@ def initialize(type, data = nil) expect(update.change_set.changes[0].version).to eq(101) end + it "ignores put and delete objects of unknown kinds" do + server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.new( + payload: LaunchDarkly::Interfaces::DataSystem::Payload.new( + id: "id", + target: 300, + code: LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL, + reason: "cant-catchup" + ) + ) + known_put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new( + version: 100, + kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG, + key: "flagkey", + object: { key: "flagkey" } + ) + unknown_put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new( + version: 100, + kind: "some-future-kind", + key: "otherkey", + object: { foo: 1 } + ) + unknown_delete = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new( + version: 101, + kind: "some-future-kind", + key: "otherkey" + ) + known_delete = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new( + version: 101, + kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT, + key: "segmentkey" + ) + selector = LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300) + + expect(logger).not_to receive(:warn) + expect(logger).not_to receive(:error) + + # Process a payload that mixes known and unknown object kinds + synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT, JSON.generate(server_intent.to_h)), +change_set_builder, envid) + [known_put, unknown_put].each do |put| + result = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT, JSON.generate(put.to_h)), +change_set_builder, envid) + expect(result).to be_nil + end + [unknown_delete, known_delete].each do |delete_object| + result = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT, JSON.generate(delete_object.to_h)), +change_set_builder, envid) + expect(result).to be_nil + end + update = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED, JSON.generate(selector.to_h)), +change_set_builder, envid) + + # The payload completes as valid with only the known objects + expect(update).not_to be_nil + expect(update.state).to eq(LaunchDarkly::Interfaces::DataSource::Status::VALID) + expect(update.error).to be_nil + expect(update.change_set).not_to be_nil + expect(update.change_set.changes.length).to eq(2) + expect(update.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT) + expect(update.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG) + expect(update.change_set.changes[0].key).to eq(:flagkey) + expect(update.change_set.changes[1].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE) + expect(update.change_set.changes[1].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT) + expect(update.change_set.changes[1].key).to eq(:segmentkey) + end + it "swallows goodbye events" do server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.new( payload: LaunchDarkly::Interfaces::DataSystem::Payload.new(