Skip to content
Open
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: "Run contract tests with hyper_rustls"
uses: ./.github/actions/contract-tests
with:
tls_feature: "rustls"
tls_feature: "hyper-rustls"
token: ${{ secrets.GITHUB_TOKEN }}

- name: "Run contract tests with hyper_tls"
Expand Down
4 changes: 2 additions & 2 deletions contract-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ reqwest = { version = "0.12.4", features = ["default", "blocking", "json"] }
async-mutex = "1.4.0"

[features]
default = ["rustls"]
rustls = ["hyper-rustls", "launchdarkly-server-sdk/rustls"]
default = ["hyper-rustls"]
hyper-rustls = ["dep:hyper-rustls", "launchdarkly-server-sdk/hyper-rustls"]
Copy link
Member

Choose a reason for hiding this comment

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

Is changing this feature name going to be a problem compatibility wise?

Copy link
Member

Choose a reason for hiding this comment

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

Or was this a new feature to the branch?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is getting merged into a separate branch that will become 3.x once the rest of the stuff is done. So I expect to break backwards compatibility with this.

tls = ["hyper-tls"]
18 changes: 10 additions & 8 deletions contract-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ struct AppState {
streaming_https_connector: StreamingHttpsConnector,
}

#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
type HttpsConnector = hyper_rustls::HttpsConnector<HttpConnector>;
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
type StreamingHttpsConnector = hyper_util::client::legacy::connect::HttpConnector;

#[cfg(feature = "tls")]
Expand All @@ -224,20 +224,22 @@ type StreamingHttpsConnector = hyper_tls::HttpsConnector<HttpConnector>;
async fn main() -> std::io::Result<()> {
env_logger::init();

#[cfg(not(any(feature = "tls", feature = "rustls")))]
#[cfg(not(any(feature = "tls", feature = "hyper-rustls")))]
{
compile_error!("one of the { \"tls\", \"rustls\" } features must be enabled");
compile_error!("one of the { \"tls\", \"hyper-rustls\" } features must be enabled");
}
#[cfg(all(feature = "tls", feature = "rustls"))]
#[cfg(all(feature = "tls", feature = "hyper-rustls"))]
{
compile_error!("only one of the { \"tls\", \"rustls\" } features can be enabled at a time");
compile_error!(
"only one of the { \"tls\", \"hyper-rustls\" } features can be enabled at a time"
);
}

let (tx, rx) = mpsc::channel::<()>();

#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
let streaming_https_connector = hyper_util::client::legacy::connect::HttpConnector::new();
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
let connector = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.expect("Failed to load native root certificates")
Expand Down
19 changes: 10 additions & 9 deletions launchdarkly-server-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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", branch = "feat/hyper-as-feature" }
eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client", default-features = false, branch = "feat/hyper-as-feature" }
futures = "0.3.12"
lazy_static = "1.4.0"
log = "0.4.14"
Expand All @@ -37,11 +37,11 @@ moka = { version = "0.12.1", features = ["sync"] }
uuid = {version = "1.2.2", features = ["v4"] }
http = "1.0"
bytes = "1.11"
hyper = { version = "1.0", features = ["client", "http1", "http2"] }
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "tokio"] }
http-body-util = { version = "0.1" }
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" }
tower = { version = "0.4", optional = true }
rand = "0.9"
flate2 = { version = "1.0.35", optional = true }
aws-lc-rs = "1.14.1"
Expand All @@ -59,14 +59,15 @@ reqwest = { version = "0.12.4", features = ["json"] }
testing_logger = "0.1.1"

[features]
default = ["rustls"]
rustls = ["hyper-rustls/http1", "hyper-rustls/http2", "eventsource-client/hyper-rustls"]
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"]
event-compression = ["flate2"]

[[example]]
name = "print_flags"
required-features = ["rustls"]
required-features = ["hyper-rustls"]

[[example]]
name = "progress"
required-features = ["rustls"]
required-features = ["hyper-rustls"]
12 changes: 7 additions & 5 deletions launchdarkly-server-sdk/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,11 @@ impl ConfigBuilder {
Ok(Box::new(NullDataSourceBuilder::new()))
}
Some(builder) => Ok(builder),
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
None => Ok(Box::new(
StreamingDataSourceBuilder::<es::HyperTransport>::new(),
)),
#[cfg(not(feature = "rustls"))]
#[cfg(not(feature = "hyper-rustls"))]
None => Err(BuildError::InvalidConfig(
"data source builder required when rustls is disabled".into(),
)),
Expand All @@ -320,11 +320,13 @@ impl ConfigBuilder {
Ok(Box::new(NullEventProcessorBuilder::new()))
}
Some(builder) => Ok(builder),
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
None => Ok(Box::new(EventProcessorBuilder::<
hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
hyper_rustls::HttpsConnector<
hyper_util::client::legacy::connect::HttpConnector,
>,
>::new())),
#[cfg(not(feature = "rustls"))]
#[cfg(not(feature = "hyper-rustls"))]
None => Err(BuildError::InvalidConfig(
"event processor factory required when rustls is disabled".into(),
)),
Expand Down
16 changes: 10 additions & 6 deletions launchdarkly-server-sdk/src/data_source_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::data_source::{DataSource, NullDataSource, PollingDataSource, Streamin
use crate::feature_requester_builders::{FeatureRequesterFactory, HyperFeatureRequesterBuilder};
use eventsource_client as es;
use http::Uri;
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
use hyper_rustls::HttpsConnectorBuilder;
use std::sync::{Arc, Mutex};
use std::time::Duration;
Expand Down Expand Up @@ -93,15 +93,15 @@ impl<T: es::HttpTransport> DataSourceFactory for StreamingDataSourceBuilder<T> {
tags: Option<String>,
) -> Result<Arc<dyn DataSource>, BuildError> {
let data_source_result = match &self.transport {
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
None => Ok(StreamingDataSource::new(
endpoints.streaming_base_url(),
sdk_key,
self.initial_reconnect_delay,
&tags,
es::HyperTransport::new_https(),
)),
#[cfg(not(feature = "rustls"))]
#[cfg(not(feature = "hyper-rustls"))]
None => Err(BuildError::InvalidConfig(
"https connector required when rustls is disabled".into(),
)),
Expand Down Expand Up @@ -243,7 +243,11 @@ impl<C> PollingDataSourceBuilder<C> {
impl<C> DataSourceFactory for PollingDataSourceBuilder<C>
where
C: tower::Service<Uri> + Clone + Send + Sync + 'static,
C::Response: hyper_util::client::legacy::connect::Connection + hyper::rt::Read + hyper::rt::Write + Send + Unpin,
C::Response: hyper_util::client::legacy::connect::Connection
+ hyper::rt::Read
+ hyper::rt::Write
+ Send
+ Unpin,
C::Future: Send + Unpin + 'static,
C::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Expand All @@ -255,7 +259,7 @@ where
) -> Result<Arc<dyn DataSource>, BuildError> {
let feature_requester_builder: Result<Box<dyn FeatureRequesterFactory>, BuildError> =
match &self.connector {
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
None => {
let connector = HttpsConnectorBuilder::new()
.with_webpki_roots()
Expand All @@ -270,7 +274,7 @@ where
connector,
)))
}
#[cfg(not(feature = "rustls"))]
#[cfg(not(feature = "hyper-rustls"))]
None => Err(BuildError::InvalidConfig(
"https connector required when rustls is disabled".into(),
)),
Expand Down
12 changes: 8 additions & 4 deletions launchdarkly-server-sdk/src/events/processor_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration;

use http::Uri;
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
use hyper_rustls::HttpsConnectorBuilder;
use launchdarkly_server_sdk_evaluation::Reference;
use thiserror::Error;
Expand Down Expand Up @@ -88,7 +88,11 @@ pub struct EventProcessorBuilder<C> {
impl<C> EventProcessorFactory for EventProcessorBuilder<C>
where
C: tower::Service<Uri> + Clone + Send + Sync + 'static,
C::Response: hyper_util::client::legacy::connect::Connection + hyper::rt::Read + hyper::rt::Write + Send + Unpin,
C::Response: hyper_util::client::legacy::connect::Connection
+ hyper::rt::Read
+ hyper::rt::Write
+ Send
+ Unpin,
C::Future: Send + Unpin + 'static,
C::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Expand Down Expand Up @@ -119,7 +123,7 @@ where
self.compress_events,
)))
} else {
#[cfg(feature = "rustls")]
#[cfg(feature = "hyper-rustls")]
{
let connector = HttpsConnectorBuilder::new()
.with_webpki_roots()
Expand All @@ -136,7 +140,7 @@ where
self.compress_events,
)))
}
#[cfg(not(feature = "rustls"))]
#[cfg(not(feature = "hyper-rustls"))]
Err(BuildError::InvalidConfig(
"https connector is required when rustls is disabled".into(),
))
Expand Down
12 changes: 10 additions & 2 deletions launchdarkly-server-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ 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;
Expand All @@ -66,6 +71,9 @@ 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";
Expand All @@ -78,8 +86,8 @@ lazy_static! {
format!("RustServerClient/{}", version_string());

// For cases where a statically empty header value are needed.
pub(crate) static ref EMPTY_HEADER: hyper::header::HeaderValue =
hyper::header::HeaderValue::from_static("");
pub(crate) static ref EMPTY_HEADER: http::HeaderValue =
http::HeaderValue::from_static("");
}

#[cfg(test)]
Expand Down
122 changes: 122 additions & 0 deletions launchdarkly-server-sdk/src/transport.rs
Copy link
Member

Choose a reason for hiding this comment

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

I assume this is basically the same as the event source one?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! 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<Box<dyn Stream<Item = Result<Bytes, TransportError>> + Send>>;

/// A pinned, boxed future for an HTTP response.
///
/// This represents the future returned by [`HttpTransport::request`].
pub type ResponseFuture =
Pin<Box<dyn Future<Output = Result<Response<ByteStream>, TransportError>> + Send>>;

/// 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<dyn StdError + Send + Sync + 'static>,
}

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<T: HttpTransport> {
/// inner: T,
/// }
///
/// impl<T: HttpTransport> HttpTransport for LoggingTransport<T> {
/// fn request(&self, request: Request<Bytes>) -> 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<Bytes>) -> ResponseFuture;
}
Loading