Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion launchdarkly-server-sdk.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Gem::Specification.new do |spec|

spec.add_runtime_dependency "benchmark", "~> 0.1", ">= 0.1.1"
spec.add_runtime_dependency "concurrent-ruby", "~> 1.1"
spec.add_runtime_dependency "ld-eventsource", "2.4.0"
spec.add_runtime_dependency "ld-eventsource", "2.5.0"
spec.add_runtime_dependency "observer", "~> 0.1.2"
spec.add_runtime_dependency "openssl", ">= 3.1.2", "< 5.0"
spec.add_runtime_dependency "semantic", "~> 1.6"
Expand Down
21 changes: 19 additions & 2 deletions lib/ldclient-rb/impl/data_system/streaming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ def sync(ss)
}

@sse = SSE::Client.new(base_uri, **opts) do |client|
client.on_connect do |headers|
# Extract environment ID and check for fallback on successful connection
if headers
envid = headers[LD_ENVID_HEADER] || envid

# Check for fallback header on connection
if headers[LD_FD_FALLBACK_HEADER] == 'true'
log_connection_result(true)
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
revert_to_fdv1: true,
environment_id: envid
)
stop
end
end
end

client.on_event do |event|
begin
update = process_message(event, change_set_builder, envid)
Expand Down Expand Up @@ -126,8 +144,7 @@ def sync(ss)

# Extract envid and fallback from error headers if available
if error.respond_to?(:headers) && error.headers
envid_from_error = error.headers[LD_ENVID_HEADER]
envid = envid_from_error if envid_from_error
envid = error.headers[LD_ENVID_HEADER] || envid

if error.headers[LD_FD_FALLBACK_HEADER] == 'true'
fallback = true
Expand Down
136 changes: 136 additions & 0 deletions spec/impl/data_system/streaming_headers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# frozen_string_literal: true

require "spec_helper"
require "ldclient-rb/impl/data_system/streaming"
require "ldclient-rb/interfaces"

module LaunchDarkly
module Impl
module DataSystem
RSpec.describe "StreamingDataSource FDv1 fallback header detection" do
let(:logger) { double("Logger", info: nil, warn: nil, error: nil, debug: nil) }
let(:sdk_key) { "test-sdk-key" }
let(:config) do
double(
"Config",
logger: logger,
stream_uri: "https://stream.example.com",
payload_filter_key: nil,
socket_factory: nil,
initial_reconnect_delay: 1,
instance_id: nil
)
end

let(:synchronizer) { StreamingDataSource.new(sdk_key, config) }

describe "on_error callback" do
it "triggers FDv1 fallback when X-LD-FD-FALLBACK header is true" do
error_with_fallback = SSE::Errors::HTTPStatusError.new(
503,
"Service Unavailable",
{
"x-launchdarkly-fd-fallback" => "true",
"x-launchdarkly-env-id" => "test-env-123",
}
)

update = synchronizer.send(:handle_error, error_with_fallback, "test-env-123", true)

expect(update.revert_to_fdv1).to be true
expect(update.state).to eq(LaunchDarkly::Interfaces::DataSource::Status::OFF)
expect(update.environment_id).to eq("test-env-123")
end

it "does not trigger fallback when header is absent" do
error_without_fallback = SSE::Errors::HTTPStatusError.new(
503,
"Service Unavailable",
{
"x-launchdarkly-env-id" => "test-env-456",
}
)

update = synchronizer.send(:handle_error, error_without_fallback, "test-env-456", false)

expect(update.revert_to_fdv1).to be_falsy
expect(update.state).to eq(LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED)
end

it "does not trigger fallback when header value is not 'true'" do
error = SSE::Errors::HTTPStatusError.new(
503,
"Not a fallback",
{
"x-launchdarkly-fd-fallback" => "false",
}
)

# Simulate the header extraction logic from on_error callback (lines 128-135)
fallback = false
if error.respond_to?(:headers) && error.headers
fallback = true if error.headers["x-launchdarkly-fd-fallback"] == 'true'
end

expect(fallback).to be false
end

it "handles errors without headers gracefully" do
# Old version of ld-eventsource gem might not have headers
error_no_headers = SSE::Errors::HTTPStatusError.new(500, "Internal Server Error")

expect(error_no_headers.headers).to be_nil

update = synchronizer.send(:handle_error, error_no_headers, nil, false)
expect(update).not_to be_nil
end
end

describe "on_connect callback" do
it "extracts environment ID from connection headers" do
# Simulate HTTP::Headers object from SSE client
headers = double("HTTP::Headers")
allow(headers).to receive(:[]).with("x-launchdarkly-env-id").and_return("env-from-connect")
allow(headers).to receive(:[]).with("x-launchdarkly-fd-fallback").and_return(nil)

# Verify the logic that would be in on_connect
envid = nil
if headers
envid_from_headers = headers["x-launchdarkly-env-id"]
envid = envid_from_headers if envid_from_headers
end

expect(envid).to eq("env-from-connect")
end

it "detects fallback header on connection" do
headers = double("HTTP::Headers")
allow(headers).to receive(:[]).with("x-launchdarkly-env-id").and_return("env-123")
allow(headers).to receive(:[]).with("x-launchdarkly-fd-fallback").and_return("true")

# Verify the logic that would trigger fallback on connect
should_fallback = false
if headers && headers["x-launchdarkly-fd-fallback"] == 'true'
should_fallback = true
end

expect(should_fallback).to be true
end

it "does not trigger fallback when header is missing on connection" do
headers = double("HTTP::Headers")
allow(headers).to receive(:[]).with("x-launchdarkly-env-id").and_return("env-456")
allow(headers).to receive(:[]).with("x-launchdarkly-fd-fallback").and_return(nil)

should_fallback = false
if headers && headers["x-launchdarkly-fd-fallback"] == 'true'
should_fallback = true
end

expect(should_fallback).to be false
end
end
end
end
end
end