diff --git a/.github/actions/build-docs/action.yml b/.github/actions/build-docs/action.yml index 01685b5..480322e 100644 --- a/.github/actions/build-docs/action.yml +++ b/.github/actions/build-docs/action.yml @@ -6,4 +6,4 @@ runs: steps: - name: Build Documentation shell: bash - run: cargo doc --no-deps -p launchdarkly-server-sdk + run: cargo doc --no-deps --all-features -p launchdarkly-server-sdk diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 45648a7..3040232 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -1,6 +1,12 @@ name: CI Workflow description: 'Shared CI workflow.' +inputs: + feature-flags: + description: 'Cargo feature flags to pass to test and clippy commands' + required: false + default: '' + runs: using: composite steps: @@ -10,8 +16,8 @@ runs: - name: Run tests shell: bash - run: cargo test -p launchdarkly-server-sdk + run: cargo test ${{ inputs.feature-flags }} -p launchdarkly-server-sdk - name: Run clippy checks shell: bash - run: cargo clippy -p launchdarkly-server-sdk -- -D warnings + run: cargo clippy ${{ inputs.feature-flags }} -p launchdarkly-server-sdk -- -D warnings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e4880f..466d017 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,20 @@ on: jobs: ci-build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + features: + - name: "default" + flags: "" + - name: "no-features" + flags: "--no-default-features" + - name: "hyper" + flags: "--no-default-features --features hyper" + - name: "hyper-rustls" + flags: "--no-default-features --features hyper-rustls" + + name: CI (${{ matrix.features.name }}) steps: - uses: actions/checkout@v4 @@ -28,6 +42,25 @@ jobs: rustup component add rustfmt clippy - uses: ./.github/actions/ci + with: + feature-flags: ${{ matrix.features.flags }} + + contract-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get Rust version + id: rust-version + run: cat ./.github/variables/rust-versions.env >> $GITHUB_OUTPUT + + - name: Setup rust tooling + run: | + rustup override set ${{ steps.rust-version.outputs.target }} + rustup component add rustfmt clippy - name: "Run contract tests with hyper_rustls" uses: ./.github/actions/contract-tests @@ -41,6 +74,23 @@ jobs: tls_feature: "tls" token: ${{ secrets.GITHUB_TOKEN }} + build-docs: + runs-on: ubuntu-latest + name: Build Documentation (all features) + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get Rust version + id: rust-version + run: cat ./.github/variables/rust-versions.env >> $GITHUB_OUTPUT + + - name: Setup rust tooling + run: | + rustup override set ${{ steps.rust-version.outputs.target }} + - uses: ./.github/actions/build-docs musl-build: diff --git a/contract-tests/Cargo.toml b/contract-tests/Cargo.toml index 6b12a38..1f48685 100644 --- a/contract-tests/Cargo.toml +++ b/contract-tests/Cargo.toml @@ -9,8 +9,8 @@ license = "Apache-2.0" actix = "0.13.0" actix-web = "4.2.1" env_logger = "0.10.0" -# eventsource-client = { version = "0.16.0", default-features = false } -eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client", branch = "feat/hyper-as-feature" } +eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client.git", branch = "feat/hyper-as-feature" } +launchdarkly-sdk-transport = { version = "0.1.0" } log = "0.4.14" launchdarkly-server-sdk = { path = "../launchdarkly-server-sdk/", default-features = false, features = ["event-compression"]} serde = { version = "1.0.132", features = ["derive"] } diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index 1a72648..14dffed 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -38,10 +38,12 @@ impl ClientEntity { connector: HttpsConnector, ) -> Result { // Create fresh transports for this client to avoid shared connection pool issues - let transport = - launchdarkly_server_sdk::HyperTransport::new_with_connector(connector.clone()); - let streaming_https_transport = - eventsource_client::HyperTransport::builder().build_with_connector(connector.clone()); + let transport = launchdarkly_sdk_transport::HyperTransport::builder() + .build_with_connector(connector.clone()) + .map_err(|e| BuildError::InvalidConfig(e.to_string()))?; + let streaming_https_transport = launchdarkly_sdk_transport::HyperTransport::builder() + .build_with_connector(connector.clone()) + .map_err(|e| BuildError::InvalidConfig(e.to_string()))?; let mut config_builder = ConfigBuilder::new(&create_instance_params.configuration.credential); diff --git a/launchdarkly-server-sdk/Cargo.toml b/launchdarkly-server-sdk/Cargo.toml index 130cc8d..7f5b16d 100644 --- a/launchdarkly-server-sdk/Cargo.toml +++ b/launchdarkly-server-sdk/Cargo.toml @@ -20,8 +20,8 @@ features = ["event-compression"] chrono = "0.4.19" crossbeam-channel = "0.5.1" data-encoding = "2.3.2" -# eventsource-client = { version = "0.16.0", default-features = false } -eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client", default-features = false, branch = "feat/hyper-as-feature" } +eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client.git", branch = "feat/hyper-as-feature" } +launchdarkly-sdk-transport = { version = "0.1.0" } futures = "0.3.12" log = "0.4.14" lru = { version = "0.16.3", default-features = false } @@ -37,11 +37,6 @@ uuid = {version = "1.2.2", features = ["v4"] } http = "1.0" bytes = "1.11" bitflags = "2.4" -hyper = { version = "1.0", features = ["client", "http1", "http2"], optional = true } -hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "tokio"], optional = true } -http-body-util = { version = "0.1", optional = true } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "webpki-roots"], optional = true} -tower = { version = "0.4", optional = true } rand = "0.9" flate2 = { version = "1.0.35", optional = true } aws-lc-rs = "1.14.1" @@ -60,8 +55,8 @@ testing_logger = "0.1.1" [features] default = ["hyper-rustls"] -hyper = ["dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:tower", "eventsource-client/hyper"] -hyper-rustls = ["dep:hyper-rustls", "hyper", "eventsource-client/hyper-rustls"] +hyper = ["launchdarkly-sdk-transport/hyper", "eventsource-client/hyper"] +hyper-rustls = ["hyper", "launchdarkly-sdk-transport/hyper-rustls", "eventsource-client/hyper-rustls"] event-compression = ["flate2"] [[example]] diff --git a/launchdarkly-server-sdk/examples/custom_transport.rs b/launchdarkly-server-sdk/examples/custom_transport.rs index ecdb895..9867abf 100644 --- a/launchdarkly-server-sdk/examples/custom_transport.rs +++ b/launchdarkly-server-sdk/examples/custom_transport.rs @@ -1,8 +1,7 @@ use bytes::Bytes; use http::Request; -use launchdarkly_server_sdk::{ - ConfigBuilder, EventProcessorBuilder, HttpTransport, ResponseFuture, -}; +use launchdarkly_sdk_transport::{HttpTransport, ResponseFuture}; +use launchdarkly_server_sdk::{ConfigBuilder, EventProcessorBuilder}; use std::time::Instant; /// Example of a custom transport that wraps another transport and adds logging. @@ -21,7 +20,7 @@ impl LoggingTransport { } impl HttpTransport for LoggingTransport { - fn request(&self, request: Request) -> ResponseFuture { + fn request(&self, request: Request>) -> ResponseFuture { let method = request.method().clone(); let uri = request.uri().clone(); let start = Instant::now(); @@ -65,7 +64,7 @@ async fn main() -> Result<(), Box> { } // Create the base HTTPS transport - let base_transport = launchdarkly_server_sdk::HyperTransport::new_https(); + let base_transport = launchdarkly_sdk_transport::HyperTransport::new_https()?; // Wrap it with logging middleware let logging_transport = LoggingTransport::new(base_transport); diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 0646111..39af0d8 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -2722,7 +2722,7 @@ mod tests { .daemon_mode(daemon_mode) .data_source(MockDataSourceBuilder::new().data_source(updates)) .event_processor( - EventProcessorBuilder::::new() + EventProcessorBuilder::::new() .event_sender(Arc::new(event_sender)), ) .build() diff --git a/launchdarkly-server-sdk/src/config.rs b/launchdarkly-server-sdk/src/config.rs index 0c025cd..07bd8a7 100644 --- a/launchdarkly-server-sdk/src/config.rs +++ b/launchdarkly-server-sdk/src/config.rs @@ -6,7 +6,6 @@ use crate::events::processor_builders::{ }; use crate::stores::store_builders::{DataStoreFactory, InMemoryDataStoreBuilder}; use crate::{ServiceEndpointsBuilder, StreamingDataSourceBuilder}; -use eventsource_client as es; use std::borrow::Borrow; @@ -303,7 +302,13 @@ impl ConfigBuilder { Some(builder) => Ok(builder), #[cfg(feature = "hyper-rustls")] None => { - let transport = es::HyperTransport::new_https(); + let transport = launchdarkly_sdk_transport::HyperTransport::new_https() + .map_err(|e| { + BuildError::InvalidConfig(format!( + "failed to create default transport: {}", + e + )) + })?; let mut builder = StreamingDataSourceBuilder::new(); builder.transport(transport); Ok(Box::new(builder)) @@ -325,7 +330,13 @@ impl ConfigBuilder { Some(builder) => Ok(builder), #[cfg(feature = "hyper-rustls")] None => { - let transport = crate::HyperTransport::new_https(); + let transport = launchdarkly_sdk_transport::HyperTransport::new_https() + .map_err(|e| { + BuildError::InvalidConfig(format!( + "failed to create default transport: {}", + e + )) + })?; let mut builder = EventProcessorBuilder::new(); builder.transport(transport); Ok(Box::new(builder)) diff --git a/launchdarkly-server-sdk/src/data_source.rs b/launchdarkly-server-sdk/src/data_source.rs index f1c8e23..47b5c88 100644 --- a/launchdarkly-server-sdk/src/data_source.rs +++ b/launchdarkly-server-sdk/src/data_source.rs @@ -70,7 +70,7 @@ pub struct StreamingDataSource { impl StreamingDataSource { #[allow(clippy::result_large_err)] - pub fn new( + pub fn new( base_url: &str, sdk_key: &str, initial_reconnect_delay: Duration, @@ -375,7 +375,6 @@ mod tests { use super::{DataSource, PollingDataSource, StreamingDataSource}; use crate::feature_requester_builders::HttpFeatureRequesterBuilder; use crate::{stores::store::InMemoryDataStore, LAUNCHDARKLY_TAGS_HEADER}; - use eventsource_client as es; #[test_case(Some("application-id/abc:application-sha/xyz".into()), "application-id/abc:application-sha/xyz")] #[test_case(None, Matcher::Missing)] @@ -402,7 +401,8 @@ mod tests { "sdk-key", Duration::from_secs(0), &tag, - es::HyperTransport::new(), + launchdarkly_sdk_transport::HyperTransport::new() + .expect("Failed to create streaming data source"), ) .unwrap(); @@ -453,7 +453,8 @@ mod tests { let (shutdown_tx, _) = broadcast::channel::<()>(1); let initialized = Arc::new(AtomicBool::new(false)); - let transport = crate::HyperTransport::new(); + let transport = launchdarkly_sdk_transport::HyperTransport::new() + .expect("Failed to create transport for polling data source"); let hyper_builder = HttpFeatureRequesterBuilder::new(&server.url(), "sdk-key", transport); let polling = PollingDataSource::new( diff --git a/launchdarkly-server-sdk/src/data_source_builders.rs b/launchdarkly-server-sdk/src/data_source_builders.rs index 39ed7d7..b060111 100644 --- a/launchdarkly-server-sdk/src/data_source_builders.rs +++ b/launchdarkly-server-sdk/src/data_source_builders.rs @@ -1,8 +1,7 @@ use super::service_endpoints; use crate::data_source::{DataSource, NullDataSource, PollingDataSource, StreamingDataSource}; use crate::feature_requester_builders::{FeatureRequesterFactory, HttpFeatureRequesterBuilder}; -use crate::transport::HttpTransport; -use eventsource_client as es; +use launchdarkly_sdk_transport::HttpTransport; use std::sync::{Arc, Mutex}; use std::time::Duration; use thiserror::Error; @@ -45,20 +44,20 @@ pub trait DataSourceFactory { /// Adjust the initial reconnect delay. /// ``` /// # use launchdarkly_server_sdk::{StreamingDataSourceBuilder, ConfigBuilder}; -/// # use eventsource_client as es; +/// # use launchdarkly_sdk_transport::HyperTransport; /// # use std::time::Duration; /// # fn main() { -/// ConfigBuilder::new("sdk-key").data_source(StreamingDataSourceBuilder::::new() +/// ConfigBuilder::new("sdk-key").data_source(StreamingDataSourceBuilder::::new() /// .initial_reconnect_delay(Duration::from_secs(10))); /// # } /// ``` #[derive(Clone)] -pub struct StreamingDataSourceBuilder { +pub struct StreamingDataSourceBuilder { initial_reconnect_delay: Duration, transport: Option, } -impl StreamingDataSourceBuilder { +impl StreamingDataSourceBuilder { /// Create a new instance of the [StreamingDataSourceBuilder] with default values. pub fn new() -> Self { Self { @@ -83,7 +82,9 @@ impl StreamingDataSourceBuilder { } } -impl DataSourceFactory for StreamingDataSourceBuilder { +impl DataSourceFactory + for StreamingDataSourceBuilder +{ fn build( &self, endpoints: &service_endpoints::ServiceEndpoints, @@ -92,13 +93,21 @@ impl DataSourceFactory for StreamingDataSourceBuilder { ) -> Result, BuildError> { let data_source_result = match &self.transport { #[cfg(feature = "hyper-rustls")] - None => Ok(StreamingDataSource::new( - endpoints.streaming_base_url(), - sdk_key, - self.initial_reconnect_delay, - &tags, - es::HyperTransport::new_https(), - )), + None => { + let transport = + launchdarkly_sdk_transport::HyperTransport::new_https().map_err(|e| { + BuildError::InvalidConfig(format!( + "failed to create default https transport: {e:?}" + )) + })?; + Ok(StreamingDataSource::new( + endpoints.streaming_base_url(), + sdk_key, + self.initial_reconnect_delay, + &tags, + transport, + )) + } #[cfg(not(feature = "hyper-rustls"))] None => Err(BuildError::InvalidConfig( "https connector required when rustls is disabled".into(), @@ -121,7 +130,7 @@ impl DataSourceFactory for StreamingDataSourceBuilder { } } -impl Default for StreamingDataSourceBuilder { +impl Default for StreamingDataSourceBuilder { fn default() -> Self { StreamingDataSourceBuilder::new() } @@ -171,7 +180,8 @@ impl Default for NullDataSourceBuilder { /// /// Adjust the initial reconnect delay. /// ``` -/// # use launchdarkly_server_sdk::{PollingDataSourceBuilder, ConfigBuilder, HyperTransport}; +/// # use launchdarkly_server_sdk::{PollingDataSourceBuilder, ConfigBuilder}; +/// # use launchdarkly_sdk_transport::HyperTransport; /// # use std::time::Duration; /// # fn main() { /// ConfigBuilder::new("sdk-key").data_source(PollingDataSourceBuilder::::new() @@ -179,7 +189,7 @@ impl Default for NullDataSourceBuilder { /// # } /// ``` #[derive(Clone)] -pub struct PollingDataSourceBuilder { +pub struct PollingDataSourceBuilder { poll_interval: Duration, transport: Option, } @@ -199,7 +209,8 @@ pub struct PollingDataSourceBuilder { /// /// Adjust the poll interval. /// ``` -/// # use launchdarkly_server_sdk::{PollingDataSourceBuilder, ConfigBuilder, HyperTransport}; +/// # use launchdarkly_server_sdk::{PollingDataSourceBuilder, ConfigBuilder}; +/// # use launchdarkly_sdk_transport::HyperTransport; /// # use std::time::Duration; /// # fn main() { /// ConfigBuilder::new("sdk-key").data_source(PollingDataSourceBuilder::::new() @@ -245,7 +256,12 @@ impl DataSourceFactory for PollingDataSourceBuilder { match &self.transport { #[cfg(feature = "hyper-rustls")] None => { - let transport = crate::HyperTransport::new_https(); + let transport = launchdarkly_sdk_transport::HyperTransport::new_https() + .map_err(|e| { + BuildError::InvalidConfig(format!( + "failed to create default https transport: {e:?}" + )) + })?; Ok(Box::new(HttpFeatureRequesterBuilder::new( endpoints.polling_base_url(), @@ -323,13 +339,14 @@ impl DataSourceFactory for MockDataSourceBuilder { #[cfg(test)] mod tests { - use eventsource_client::{HyperTransport, ResponseFuture}; + use bytes::Bytes; + use launchdarkly_sdk_transport::{HyperTransport, Request, ResponseFuture}; use super::*; #[test] fn default_stream_builder_has_correct_defaults() { - let builder: StreamingDataSourceBuilder = + let builder: StreamingDataSourceBuilder = StreamingDataSourceBuilder::new(); assert_eq!( @@ -343,11 +360,8 @@ mod tests { #[derive(Debug, Clone)] struct TestTransport; - impl es::HttpTransport for TestTransport { - fn request( - &self, - _request: eventsource_client::Request>, - ) -> ResponseFuture { + impl launchdarkly_sdk_transport::HttpTransport for TestTransport { + fn request(&self, _request: Request>) -> ResponseFuture { // this won't be called during the test unreachable!(); } @@ -366,7 +380,7 @@ mod tests { #[test] fn default_polling_builder_has_correct_defaults() { - let builder = PollingDataSourceBuilder::::new(); + let builder = PollingDataSourceBuilder::::new(); assert_eq!(builder.poll_interval, MINIMUM_POLL_INTERVAL,); } diff --git a/launchdarkly-server-sdk/src/events/processor_builders.rs b/launchdarkly-server-sdk/src/events/processor_builders.rs index 5aa1cf4..1d4ebf9 100644 --- a/launchdarkly-server-sdk/src/events/processor_builders.rs +++ b/launchdarkly-server-sdk/src/events/processor_builders.rs @@ -9,8 +9,8 @@ use launchdarkly_server_sdk_evaluation::Reference; use thiserror::Error; use crate::events::sender::HttpEventSender; -use crate::transport::HttpTransport; use crate::{service_endpoints, LAUNCHDARKLY_TAGS_HEADER}; +use launchdarkly_sdk_transport::HttpTransport; use super::processor::{ EventProcessor, EventProcessorError, EventProcessorImpl, NullEventProcessor, @@ -60,7 +60,8 @@ pub trait EventProcessorFactory { /// /// Adjust the flush interval /// ``` -/// # use launchdarkly_server_sdk::{EventProcessorBuilder, ConfigBuilder, HyperTransport}; +/// # use launchdarkly_server_sdk::{EventProcessorBuilder, ConfigBuilder}; +/// # use launchdarkly_sdk_transport::HyperTransport; /// # use std::time::Duration; /// # fn main() { /// ConfigBuilder::new("sdk-key").event_processor(EventProcessorBuilder::::new() @@ -68,7 +69,7 @@ pub trait EventProcessorFactory { /// # } /// ``` #[derive(Clone)] -pub struct EventProcessorBuilder { +pub struct EventProcessorBuilder { capacity: usize, flush_interval: Duration, context_keys_capacity: NonZeroUsize, @@ -112,7 +113,12 @@ impl EventProcessorFactory for EventProcessorBuilder { } else { #[cfg(feature = "hyper-rustls")] { - let transport = crate::HyperTransport::new_https(); + let transport = launchdarkly_sdk_transport::HyperTransport::new_https().map_err(|e| { + BuildError::InvalidConfig(format!( + "failed to create default https transport: {}", + e + )) + })?; Ok(Arc::new(HttpEventSender::new( transport, Uri::from_str(url_string.as_str()).unwrap(), @@ -317,28 +323,31 @@ mod tests { #[test] fn default_builder_has_correct_defaults() { - let builder = EventProcessorBuilder::::new(); + let builder = EventProcessorBuilder::::new(); assert_eq!(builder.capacity, DEFAULT_EVENT_CAPACITY); assert_eq!(builder.flush_interval, DEFAULT_FLUSH_POLL_INTERVAL); } #[test] fn capacity_can_be_adjusted() { - let mut builder = EventProcessorBuilder::::new(); + let mut builder = + EventProcessorBuilder::::new(); builder.capacity(1234); assert_eq!(builder.capacity, 1234); } #[test] fn flush_interval_can_be_adjusted() { - let mut builder = EventProcessorBuilder::::new(); + let mut builder = + EventProcessorBuilder::::new(); builder.flush_interval(Duration::from_secs(1234)); assert_eq!(builder.flush_interval, Duration::from_secs(1234)); } #[test] fn context_keys_capacity_can_be_adjusted() { - let mut builder = EventProcessorBuilder::::new(); + let mut builder = + EventProcessorBuilder::::new(); let cap = NonZeroUsize::new(1234).expect("1234 > 0"); builder.context_keys_capacity(cap); assert_eq!(builder.context_keys_capacity, cap); @@ -346,7 +355,8 @@ mod tests { #[test] fn context_keys_flush_interval_can_be_adjusted() { - let mut builder = EventProcessorBuilder::::new(); + let mut builder = + EventProcessorBuilder::::new(); builder.context_keys_flush_interval(Duration::from_secs(1000)); assert_eq!( builder.context_keys_flush_interval, @@ -356,7 +366,8 @@ mod tests { #[test] fn all_attribute_private_can_be_adjusted() { - let mut builder = EventProcessorBuilder::::new(); + let mut builder = + EventProcessorBuilder::::new(); assert!(!builder.all_attributes_private); builder.all_attributes_private(true); @@ -365,7 +376,8 @@ mod tests { #[test] fn attribte_names_can_be_adjusted() { - let mut builder = EventProcessorBuilder::::new(); + let mut builder = + EventProcessorBuilder::::new(); assert!(builder.private_attributes.is_empty()); builder.private_attributes(hashset!["name"]); @@ -390,7 +402,7 @@ mod tests { .build() .expect("Service endpoints failed to be created"); - let builder = EventProcessorBuilder::::new(); + let builder = EventProcessorBuilder::::new(); let processor = builder .build(&service_endpoints, "sdk-key", tag) .expect("Processor failed to build"); diff --git a/launchdarkly-server-sdk/src/events/sender.rs b/launchdarkly-server-sdk/src/events/sender.rs index 3af8532..162e84c 100644 --- a/launchdarkly-server-sdk/src/events/sender.rs +++ b/launchdarkly-server-sdk/src/events/sender.rs @@ -1,9 +1,10 @@ use crate::{ - reqwest::is_http_error_recoverable, transport::HttpTransport, LAUNCHDARKLY_EVENT_SCHEMA_HEADER, + reqwest::is_http_error_recoverable, LAUNCHDARKLY_EVENT_SCHEMA_HEADER, LAUNCHDARKLY_PAYLOAD_ID_HEADER, }; use chrono::DateTime; use crossbeam_channel::Sender; +use launchdarkly_sdk_transport::HttpTransport; use std::collections::HashMap; #[cfg(feature = "event-compression")] @@ -146,7 +147,7 @@ impl EventSender for HttpEventSender { // Create request with Bytes body for transport let body_bytes = Bytes::from(payload.clone()); - let request = request_builder.body(body_bytes).unwrap(); + let request = request_builder.body(Some(body_bytes)).unwrap(); let result = self.transport.request(request).await; @@ -223,7 +224,7 @@ impl EventSender for InMemoryEventSender { events: Vec, sender: Sender, flush_signal: Option>, - ) -> BoxFuture<()> { + ) -> BoxFuture<'_, ()> { Box::pin(async move { for event in events { self.event_tx.send(event).unwrap(); @@ -248,18 +249,18 @@ mod tests { use std::str::FromStr; use test_case::test_case; - #[test_case(hyper::StatusCode::CONTINUE, true)] - #[test_case(hyper::StatusCode::OK, true)] - #[test_case(hyper::StatusCode::MULTIPLE_CHOICES, true)] - #[test_case(hyper::StatusCode::BAD_REQUEST, true)] - #[test_case(hyper::StatusCode::UNAUTHORIZED, false)] - #[test_case(hyper::StatusCode::REQUEST_TIMEOUT, true)] - #[test_case(hyper::StatusCode::CONFLICT, false)] - #[test_case(hyper::StatusCode::TOO_MANY_REQUESTS, true)] - #[test_case(hyper::StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, false)] - #[test_case(hyper::StatusCode::INTERNAL_SERVER_ERROR, true)] - fn can_determine_recoverable_errors(status: hyper::StatusCode, is_recoverable: bool) { - assert_eq!(is_recoverable, is_http_error_recoverable(status.as_u16())); + #[test_case(100, true; "100 CONTINUE is recoverable")] + #[test_case(200, true; "200 OK is recoverable")] + #[test_case(300, true; "300 MULTIPLE_CHOICES is recoverable")] + #[test_case(400, true; "400 BAD_REQUEST is recoverable")] + #[test_case(401, false; "401 UNAUTHORIZED is not recoverable")] + #[test_case(408, true; "408 REQUEST_TIMEOUT is recoverable")] + #[test_case(409, false; "409 CONFLICT is not recoverable")] + #[test_case(429, true; "429 TOO_MANY_REQUESTS is recoverable")] + #[test_case(431, false; "431 REQUEST_HEADER_FIELDS_TOO_LARGE is not recoverable")] + #[test_case(500, true; "500 INTERNAL_SERVER_ERROR is recoverable")] + fn can_determine_recoverable_errors(status: u16, is_recoverable: bool) { + assert_eq!(is_recoverable, is_http_error_recoverable(status)); } #[tokio::test] @@ -349,11 +350,14 @@ mod tests { assert_eq!(sender_result.time_from_server, 1234567890000); } - fn build_event_sender(url: String) -> HttpEventSender { + fn build_event_sender( + url: String, + ) -> HttpEventSender { let url = format!("{}/bulk", &url); let url = http::Uri::from_str(&url).expect("Failed parsing the mock server url"); - let transport = crate::HyperTransport::new(); + let transport = launchdarkly_sdk_transport::HyperTransport::new() + .expect("Failed to create HyperTransport"); HttpEventSender::new( transport, url, diff --git a/launchdarkly-server-sdk/src/feature_requester.rs b/launchdarkly-server-sdk/src/feature_requester.rs index 358bcdb..2c65f5e 100644 --- a/launchdarkly-server-sdk/src/feature_requester.rs +++ b/launchdarkly-server-sdk/src/feature_requester.rs @@ -1,8 +1,8 @@ use crate::reqwest::is_http_error_recoverable; -use crate::transport::HttpTransport; use bytes::Bytes; use futures::future::BoxFuture; use futures::stream::StreamExt; +use launchdarkly_sdk_transport::HttpTransport; use std::collections::HashMap; use super::stores::store_types::AllData; @@ -72,7 +72,7 @@ impl FeatureRequester for HttpFeatureRequester { } // Create empty body for GET request - let request = request_builder.body(Bytes::new()).unwrap(); + let request = request_builder.body(Some(Bytes::new())).unwrap(); let result = transport.request(request).await; @@ -86,7 +86,8 @@ impl FeatureRequester for HttpFeatureRequester { } }; - if response.status() == hyper::StatusCode::NOT_MODIFIED && cache.is_some() { + // 304 NOT MODIFIED + if response.status() == 304 && cache.is_some() { if let Some(entry) = cache { return Ok(entry.0); } @@ -246,9 +247,12 @@ mod tests { } } - fn build_feature_requester(url: String) -> HttpFeatureRequester { + fn build_feature_requester( + url: String, + ) -> HttpFeatureRequester { let url = http::Uri::from_str(&url).expect("Failed parsing the mock server url"); - let transport = crate::HyperTransport::new(); + let transport = launchdarkly_sdk_transport::HyperTransport::new() + .expect("Failed to create HyperTransport"); HttpFeatureRequester::new( transport, diff --git a/launchdarkly-server-sdk/src/feature_requester_builders.rs b/launchdarkly-server-sdk/src/feature_requester_builders.rs index 448fbd0..3c29f40 100644 --- a/launchdarkly-server-sdk/src/feature_requester_builders.rs +++ b/launchdarkly-server-sdk/src/feature_requester_builders.rs @@ -1,7 +1,7 @@ use crate::feature_requester::{FeatureRequester, HttpFeatureRequester}; -use crate::transport::HttpTransport; use crate::LAUNCHDARKLY_TAGS_HEADER; use http::Uri; +use launchdarkly_sdk_transport::HttpTransport; use std::collections::HashMap; use std::str::FromStr; use thiserror::Error; @@ -68,7 +68,8 @@ mod tests { #[test] fn factory_handles_url_parsing_failure() { - let transport = crate::HyperTransport::new(); + let transport = + launchdarkly_sdk_transport::HyperTransport::new().expect("Failed to create transport"); let builder = HttpFeatureRequesterBuilder::new( "This is clearly not a valid URL", "sdk-key", diff --git a/launchdarkly-server-sdk/src/lib.rs b/launchdarkly-server-sdk/src/lib.rs index ccfa7a8..7dd7b1d 100644 --- a/launchdarkly-server-sdk/src/lib.rs +++ b/launchdarkly-server-sdk/src/lib.rs @@ -16,12 +16,12 @@ extern crate log; #[macro_use] extern crate serde_json; +use http::HeaderValue; pub use launchdarkly_server_sdk_evaluation::Error as EvalError; pub use launchdarkly_server_sdk_evaluation::{ AttributeValue, Context, ContextBuilder, Detail, FlagValue, Kind, MultiContextBuilder, Reason, Reference, }; -use http::HeaderValue; use std::sync::LazyLock; pub use client::Client; @@ -53,11 +53,6 @@ pub use stores::persistent_store_builders::{ pub use stores::store_types::{AllData, DataKind, SerializedItem, StorageItem}; pub use version::version_string; -// Re-export transport types -pub use transport::{HttpTransport, ResponseFuture, TransportError}; -#[cfg(feature = "hyper")] -pub use transport_hyper::HyperTransport; - mod client; mod config; mod data_source; @@ -72,9 +67,6 @@ mod sampler; mod service_endpoints; mod stores; mod test_common; -mod transport; -#[cfg(feature = "hyper")] -mod transport_hyper; mod version; static LAUNCHDARKLY_EVENT_SCHEMA_HEADER: &str = "x-launchdarkly-event-schema"; @@ -85,8 +77,7 @@ static CURRENT_EVENT_SCHEMA: &str = "4"; static USER_AGENT: LazyLock = LazyLock::new(|| format!("RustServerClient/{}", version_string())); -static EMPTY_HEADER: LazyLock = - LazyLock::new(|| HeaderValue::from_static("")); +static EMPTY_HEADER: LazyLock = LazyLock::new(|| HeaderValue::from_static("")); #[cfg(test)] mod tests { diff --git a/launchdarkly-server-sdk/src/reqwest.rs b/launchdarkly-server-sdk/src/reqwest.rs index 9fcc2b9..8c0d69e 100644 --- a/launchdarkly-server-sdk/src/reqwest.rs +++ b/launchdarkly-server-sdk/src/reqwest.rs @@ -1,25 +1,17 @@ -use hyper::StatusCode; - pub fn is_http_error_recoverable(status: u16) -> bool { - if let Ok(status) = StatusCode::from_u16(status) { - if !status.is_client_error() { - return true; - } - - return matches!( - status, - StatusCode::BAD_REQUEST | StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS - ); + if !(400..500).contains(&status) { + return true; } - warn!("Unable to determine if status code is recoverable"); - false + matches!( + status, + 400 | 408 | 429 // BAD_REQUEST | REQUEST_TIMEOUT | TOO_MANY_REQUESTS + ) } #[cfg(test)] mod tests { use super::*; - use hyper::StatusCode; use test_case::test_case; #[test_case("130.65331632653061", 130.65331632653061)] @@ -30,17 +22,17 @@ mod tests { assert_eq!(expected, parsed); } - #[test_case(StatusCode::CONTINUE, true)] - #[test_case(StatusCode::OK, true)] - #[test_case(StatusCode::MULTIPLE_CHOICES, true)] - #[test_case(StatusCode::BAD_REQUEST, true)] - #[test_case(StatusCode::UNAUTHORIZED, false)] - #[test_case(StatusCode::REQUEST_TIMEOUT, true)] - #[test_case(StatusCode::CONFLICT, false)] - #[test_case(StatusCode::TOO_MANY_REQUESTS, true)] - #[test_case(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, false)] - #[test_case(StatusCode::INTERNAL_SERVER_ERROR, true)] - fn can_determine_recoverable_errors(status: StatusCode, is_recoverable: bool) { - assert_eq!(is_recoverable, is_http_error_recoverable(status.as_u16())); + #[test_case(100, true; "CONTINUE_STATUS")] + #[test_case(200, true; "OK")] + #[test_case(300, true; "MULTIPLE_CHOICES")] + #[test_case(400, true; "BAD_REQUEST")] + #[test_case(401, false; "UNAUTHORIZED")] + #[test_case(408, true; "REQUEST_TIMEOUT")] + #[test_case(409, false; "CONFLICT")] + #[test_case(429, true; "TOO_MANY_REQUESTS")] + #[test_case(431, false; "REQUEST_HEADER_FIELDS_TOO_LARGE")] + #[test_case(500, true; "INTERNAL_SERVER_ERROR")] + fn can_determine_recoverable_errors(status: u16, is_recoverable: bool) { + assert_eq!(is_recoverable, is_http_error_recoverable(status)); } } diff --git a/launchdarkly-server-sdk/src/transport.rs b/launchdarkly-server-sdk/src/transport.rs deleted file mode 100644 index 92e9900..0000000 --- a/launchdarkly-server-sdk/src/transport.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! HTTP transport abstraction for LaunchDarkly SDK -//! -//! This module defines the [`HttpTransport`] trait which allows users to plug in -//! their own HTTP client implementation (hyper, reqwest, or custom). - -use bytes::Bytes; -use futures::Stream; -use std::error::Error as StdError; -use std::fmt; -use std::future::Future; -use std::pin::Pin; - -// Re-export http crate types for convenience -pub use http::{Request, Response}; - -/// A pinned, boxed stream of bytes returned by HTTP transports. -/// -/// This represents the streaming response body from an HTTP request. -pub type ByteStream = Pin> + Send + Sync>>; - -/// A pinned, boxed future for an HTTP response. -/// -/// This represents the future returned by [`HttpTransport::request`]. -pub type ResponseFuture = - Pin, TransportError>> + Send + Sync>>; - -/// Error type for HTTP transport operations. -/// -/// This wraps transport-specific errors (network failures, timeouts, etc.) in a -/// common error type that the SDK can handle uniformly. -#[derive(Debug)] -pub struct TransportError { - inner: Box, -} - -impl TransportError { - /// Create a new transport error from any error type. - pub fn new(err: impl StdError + Send + Sync + 'static) -> Self { - Self { - inner: Box::new(err), - } - } - - /// Get a reference to the inner error. - pub fn inner(&self) -> &(dyn StdError + Send + Sync + 'static) { - &*self.inner - } -} - -impl fmt::Display for TransportError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "transport error: {}", self.inner) - } -} - -impl StdError for TransportError { - fn source(&self) -> Option<&(dyn StdError + 'static)> { - Some(&*self.inner) - } -} - -/// Trait for pluggable HTTP transport implementations. -/// -/// Implement this trait to provide HTTP request/response functionality for the -/// SDK. The transport is responsible for: -/// - Establishing HTTP connections (with TLS if needed) -/// - Sending HTTP requests -/// - Returning streaming HTTP responses -/// - Handling timeouts (if desired) -/// -/// The SDK normally uses [`crate::HyperTransport`] as the default implementation, -/// but you can provide your own implementation for custom requirements such as: -/// - Using a different HTTP client library (reqwest, custom, etc.) -/// - Adding request/response logging or metrics -/// - Implementing custom retry logic -/// - Using a proxy or custom TLS configuration -/// -/// # Example -/// -/// ```no_run -/// use launchdarkly_server_sdk::{HttpTransport, ResponseFuture, TransportError}; -/// use bytes::Bytes; -/// use http::{Request, Response}; -/// -/// #[derive(Clone)] -/// struct LoggingTransport { -/// inner: T, -/// } -/// -/// impl HttpTransport for LoggingTransport { -/// fn request(&self, request: Request) -> ResponseFuture { -/// println!("Making request to: {}", request.uri()); -/// self.inner.request(request) -/// } -/// } -/// ``` -pub trait HttpTransport: Clone + Send + Sync + 'static { - /// Execute an HTTP request and return a streaming response. - /// - /// # Arguments - /// - /// * `request` - The HTTP request to execute. The body type is `Bytes` - /// to support both binary content and empty bodies. Use `Bytes::new()` - /// for requests with no body (e.g., GET requests). - /// - /// # Returns - /// - /// A future that resolves to an HTTP response with a streaming body, or a - /// transport error if the request fails. - /// - /// The response includes: - /// - Status code - /// - Response headers - /// - A stream of body bytes - /// - /// # Notes - /// - /// - The transport should NOT follow redirects - the SDK handles this when needed - /// - The transport should NOT retry requests - the SDK handles this - /// - The transport MAY implement timeouts as desired - fn request(&self, request: Request) -> ResponseFuture; -} diff --git a/launchdarkly-server-sdk/src/transport_hyper.rs b/launchdarkly-server-sdk/src/transport_hyper.rs deleted file mode 100644 index d73348e..0000000 --- a/launchdarkly-server-sdk/src/transport_hyper.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! Hyper v1 transport implementation for LaunchDarkly SDK -//! -//! This module provides a production-ready [`HyperTransport`] implementation that -//! integrates hyper v1 with the LaunchDarkly SDK. - -use crate::transport::{ByteStream, HttpTransport, ResponseFuture, TransportError}; -use bytes::Bytes; -use http::{Request, Response}; -use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; -use hyper::body::Incoming; -use hyper_util::client::legacy::Client as HyperClient; -use hyper_util::rt::TokioExecutor; - -/// A transport implementation using hyper v1.x -/// -/// This struct wraps a hyper client and implements the [`HttpTransport`] trait -/// for use with the LaunchDarkly SDK. -/// -/// # Default Configuration -/// -/// By default, `HyperTransport` uses: -/// - HTTP-only connector (no TLS) -/// - Both HTTP/1.1 and HTTP/2 protocol support -/// - No timeout configuration -/// -/// For HTTPS support, use [`HyperTransport::new_https()`] (requires the `rustls` feature) -/// or provide your own connector with [`HyperTransport::new_with_connector()`]. -/// -/// # Example -/// -/// ```ignore -/// use launchdarkly_server_sdk::{HyperTransport, ConfigBuilder, EventProcessorBuilder}; -/// -/// # #[cfg(feature = "hyper-rustls")] -/// # { -/// // Use default HTTPS transport -/// let transport = HyperTransport::new_https(); -/// -/// let config = ConfigBuilder::new("sdk-key") -/// .event_processor(EventProcessorBuilder::new().transport(transport.clone())) -/// .build(); -/// # } -/// ``` -#[derive(Clone)] -pub struct HyperTransport { - client: HyperClient>>, -} - -impl HyperTransport { - /// Create a new HyperTransport with default HTTP connector and no timeouts - /// - /// This creates a basic HTTP-only client that supports both HTTP/1 and HTTP/2. - /// For HTTPS support, use [`HyperTransport::new_https()`] instead. - /// - /// # Example - /// - /// ``` - /// use launchdarkly_server_sdk::HyperTransport; - /// - /// let transport = HyperTransport::new(); - /// ``` - pub fn new() -> Self { - let connector = hyper_util::client::legacy::connect::HttpConnector::new(); - let client = HyperClient::builder(TokioExecutor::new()).build(connector); - Self { client } - } - - /// Create a new HyperTransport with HTTPS support using rustls - /// - /// This creates an HTTPS client that supports both HTTP/1 and HTTP/2 protocols - /// with native certificate verification. - /// - /// This method is only available when the `rustls` feature is enabled. - /// - /// # Example - /// - /// ```no_run - /// # #[cfg(feature = "hyper-rustls")] - /// # { - /// use launchdarkly_server_sdk::HyperTransport; - /// - /// let transport = HyperTransport::new_https(); - /// # } - /// ``` - #[cfg(feature = "hyper-rustls")] - pub fn new_https() -> HyperTransport< - hyper_rustls::HttpsConnector, - > { - use hyper_rustls::HttpsConnectorBuilder; - - let connector = HttpsConnectorBuilder::new() - .with_webpki_roots() - .https_or_http() - .enable_http1() - .enable_http2() - .build(); - - let client = HyperClient::builder(TokioExecutor::new()).build(connector); - HyperTransport { client } - } -} - -impl HyperTransport { - /// Create a new HyperTransport with a custom connector - /// - /// This allows you to provide your own connector implementation, which is useful for: - /// - Custom TLS configuration - /// - Proxy support - /// - Connection pooling customization - /// - Custom DNS resolution - /// - /// # Example - /// - /// ```no_run - /// use launchdarkly_server_sdk::HyperTransport; - /// use hyper_util::client::legacy::connect::HttpConnector; - /// - /// let mut connector = HttpConnector::new(); - /// connector.set_nodelay(true); - /// - /// let transport = HyperTransport::new_with_connector(connector); - /// ``` - pub fn new_with_connector(connector: C) -> Self - where - C: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static, - { - let client = HyperClient::builder(TokioExecutor::new()).build(connector); - Self { client } - } -} - -impl Default for HyperTransport { - fn default() -> Self { - Self::new() - } -} - -impl HttpTransport for HyperTransport -where - C: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static, -{ - fn request(&self, request: Request) -> ResponseFuture { - let (parts, body) = request.into_parts(); - - // Convert Bytes to BoxBody for hyper - let boxed_body: BoxBody> = - if body.is_empty() { - // Use Empty for requests with no body (e.g., GET requests) - Empty::::new() - .map_err(|e| Box::new(e) as Box) - .boxed() - } else { - // Use Full for requests with a body - Full::new(body) - .map_err(|e| Box::new(e) as Box) - .boxed() - }; - - let hyper_req = hyper::Request::from_parts(parts, boxed_body); - let client = self.client.clone(); - - Box::pin(async move { - // Make the request - let resp = client - .request(hyper_req) - .await - .map_err(TransportError::new)?; - - let (parts, body) = resp.into_parts(); - - // Convert hyper's Incoming body to ByteStream - let byte_stream: ByteStream = Box::pin(body_to_stream(body)); - - Ok(Response::from_parts(parts, byte_stream)) - }) - } -} - -/// Convert hyper's Incoming body to a Stream of Bytes -fn body_to_stream( - body: Incoming, -) -> impl futures::Stream> + Send { - futures::stream::unfold(body, |mut body| async move { - match body.frame().await { - Some(Ok(frame)) => { - if let Ok(data) = frame.into_data() { - // Successfully got data frame - Some((Ok(data), body)) - } else { - // Skip non-data frames (trailers, etc.) - Some(( - Err(TransportError::new(std::io::Error::other("non-data frame"))), - body, - )) - } - } - Some(Err(e)) => { - // Error reading frame - Some((Err(TransportError::new(e)), body)) - } - None => { - // End of stream - None - } - } - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_hyper_transport_new() { - let transport = HyperTransport::new(); - // If we can create it without panic, the test passes - // This verifies the default HTTP connector is set up correctly - drop(transport); - } - - #[test] - fn test_hyper_transport_default() { - let transport = HyperTransport::default(); - // Verify Default trait implementation - drop(transport); - } - - #[cfg(feature = "hyper-rustls")] - #[test] - fn test_hyper_transport_new_https() { - let transport = HyperTransport::new_https(); - // If we can create it without panic, the test passes - // This verifies the HTTPS connector with rustls is set up correctly - drop(transport); - } - - #[test] - fn test_new_with_connector() { - use hyper_util::client::legacy::connect::HttpConnector; - - let connector = HttpConnector::new(); - let transport = HyperTransport::new_with_connector(connector); - // Verify we can build with a custom connector - drop(transport); - } -}