diff --git a/app/models/runtime/route.rb b/app/models/runtime/route.rb index bdefff78c4..53b97b0515 100644 --- a/app/models/runtime/route.rb +++ b/app/models/runtime/route.rb @@ -74,8 +74,9 @@ def options_with_serialization=(opts) cleaned_opts = remove_hash_options_for_non_hash_loadbalancing(opts) rounded_opts = round_hash_balance_to_one_decimal(cleaned_opts) normalized_opts = normalize_hash_balance_to_string(rounded_opts) - # Remove nil values after all processing - normalized_opts = normalized_opts.compact if normalized_opts.is_a?(Hash) + # Convert nil values to empty strings to signal explicit removal to downstream consumers (e.g. gorouter), + # so they can distinguish "option was explicitly removed" from "option was never set". + normalized_opts = normalized_opts.transform_values { |v| v.nil? ? '' : v } if normalized_opts.is_a?(Hash) self.options_without_serialization = Oj.dump(normalized_opts) end diff --git a/spec/unit/actions/manifest_route_update_spec.rb b/spec/unit/actions/manifest_route_update_spec.rb index 35d6e8a5e1..9178cc5fc5 100644 --- a/spec/unit/actions/manifest_route_update_spec.rb +++ b/spec/unit/actions/manifest_route_update_spec.rb @@ -650,8 +650,7 @@ module VCAP::CloudController ManifestRouteUpdate.update(app.guid, message, user_audit_info) route.reload - expect(route.options).to eq({}) - expect(route.options).not_to have_key('loadbalancing') + expect(route.options).to eq({ 'loadbalancing' => '' }) expect(route.options).not_to have_key('hash_header') expect(route.options).not_to have_key('hash_balance') end diff --git a/spec/unit/actions/route_update_spec.rb b/spec/unit/actions/route_update_spec.rb index be966d96fe..b70e0315b8 100644 --- a/spec/unit/actions/route_update_spec.rb +++ b/spec/unit/actions/route_update_spec.rb @@ -209,7 +209,7 @@ module VCAP::CloudController expect(message).to be_valid subject.update(route:, message:) route.reload - expect(route.options).to eq({}) + expect(route.options).to eq({ 'loadbalancing' => '' }) end it 'notifies the backend' do @@ -299,7 +299,7 @@ module VCAP::CloudController subject.update(route:, message:) route.reload expect(route.options).to include({ 'loadbalancing' => 'hash', 'hash_header' => 'foobar' }) - expect(route.options).not_to have_key('hash_balance') + expect(route.options['hash_balance']).to eq('') end it 'notifies the backend' do @@ -502,7 +502,7 @@ module VCAP::CloudController expect(message).to be_valid subject.update(route:, message:) route.reload - expect(route.options).to eq({}) + expect(route.options).to eq({ 'loadbalancing' => '' }) end it 'notifies the backend' do diff --git a/spec/unit/models/runtime/route_spec.rb b/spec/unit/models/runtime/route_spec.rb index c6554939ad..4e97f5a3bf 100644 --- a/spec/unit/models/runtime/route_spec.rb +++ b/spec/unit/models/runtime/route_spec.rb @@ -1346,6 +1346,23 @@ module VCAP::CloudController end end + context 'when setting an option value to nil' do + it 'serializes nil values as empty strings to signal explicit removal' do + route = Route.make( + host: 'test-route', + domain: domain, + space: space, + options: { loadbalancing: 'round-robin' } + ) + + route.update(options: { loadbalancing: nil }) + route.reload + + parsed_options = Oj.load(route.options_without_serialization) + expect(parsed_options['loadbalancing']).to eq('') + end + end + context 'when using string keys instead of symbols' do it 'still removes hash options for non-hash loadbalancing' do route = Route.make(