|
28 | 28 | //! .api_key("xyz") |
29 | 29 | //! .healthcheck_interval(Duration::from_secs(60)) |
30 | 30 | //! .retry_policy(ExponentialBackoff::builder().build_with_max_retries(3)) |
31 | | -//! .connection_timeout(Duration::from_secs(5)) |
32 | 31 | //! .build() |
33 | 32 | //! .unwrap(); |
34 | 33 | //! |
|
59 | 58 | //! ### WebAssembly (Wasm) Usage |
60 | 59 | //! |
61 | 60 | //! When compiling for a WebAssembly target (`wasm32-unknown-unknown`), |
62 | | -//! Tokio-based features such as middleware, retries, and connection |
63 | | -//! timeouts are **not available**. |
| 61 | +//! Tokio-based features such as middleware and retries are **not available**. |
64 | 62 | //! |
65 | 63 | //! Example: |
66 | 64 | //! |
|
78 | 76 | //! .nodes(vec!["http://localhost:8108"]) |
79 | 77 | //! .api_key("xyz") |
80 | 78 | //! .healthcheck_interval(Duration::from_secs(60)) |
81 | | -//! // .retry_policy(...) <-- not supported in Wasm |
82 | | -//! // .connection_timeout(...) <-- not supported in Wasm |
| 79 | +//! // .retry_policy(...) <-- not supported in Wasm |
83 | 80 | //! .build() |
84 | 81 | //! .unwrap(); |
85 | 82 | //! |
@@ -182,6 +179,136 @@ macro_rules! execute_wrapper { |
182 | 179 | }; |
183 | 180 | } |
184 | 181 |
|
| 182 | +/// Configuration for a single Typesense node. |
| 183 | +/// |
| 184 | +/// Use this to customize the HTTP client for specific nodes, |
| 185 | +/// for example to add custom TLS root certificates or configure proxies. |
| 186 | +/// |
| 187 | +/// For simple cases, you can pass a plain URL string to the builder's |
| 188 | +/// `.nodes()` method, which will be automatically converted. |
| 189 | +/// |
| 190 | +/// # Examples |
| 191 | +/// |
| 192 | +/// ``` |
| 193 | +/// use typesense::NodeConfig; |
| 194 | +/// |
| 195 | +/// // Simple URL (same as passing a string directly) |
| 196 | +/// let node = NodeConfig::new("https://node1.example.com"); |
| 197 | +/// |
| 198 | +/// // With custom HTTP client configuration |
| 199 | +/// // (add timeouts, headers, TLS, etc. on native targets) |
| 200 | +/// let node = NodeConfig::new("https://node2.example.com") |
| 201 | +/// .http_builder(|builder| { |
| 202 | +/// // This closure receives a `reqwest::ClientBuilder` and must return it. |
| 203 | +/// // You can call any supported builder methods here; for example, |
| 204 | +/// // `builder.connect_timeout(...)` on native targets. |
| 205 | +/// builder |
| 206 | +/// }); |
| 207 | +/// ``` |
| 208 | +pub struct NodeConfig { |
| 209 | + url: String, |
| 210 | + http_builder: Option<Box<dyn FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder>>, |
| 211 | +} |
| 212 | + |
| 213 | +impl std::fmt::Debug for NodeConfig { |
| 214 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 215 | + f.debug_struct("NodeConfig") |
| 216 | + .field("url", &self.url) |
| 217 | + .field("http_builder", &self.http_builder.as_ref().map(|_| "..")) |
| 218 | + .finish() |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +impl NodeConfig { |
| 223 | + /// Creates a new `NodeConfig` with the given URL. |
| 224 | + pub fn new(url: impl Into<String>) -> Self { |
| 225 | + Self { |
| 226 | + url: url.into(), |
| 227 | + http_builder: None, |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + /// Sets a custom HTTP client builder for this node. |
| 232 | + /// |
| 233 | + /// The closure receives a default [`reqwest::ClientBuilder`] and should return |
| 234 | + /// a configured builder. This is useful for adding custom TLS certificates, |
| 235 | + /// proxies, or other reqwest settings. |
| 236 | + /// |
| 237 | + /// When not set, a default builder with a 5-second connect timeout is used |
| 238 | + /// (native targets only; WASM uses the browser's defaults). |
| 239 | + /// |
| 240 | + /// # Examples |
| 241 | + /// |
| 242 | + /// ```no_run |
| 243 | + /// #[cfg(not(target_family = "wasm"))] |
| 244 | + /// { |
| 245 | + /// use typesense::NodeConfig; |
| 246 | + /// |
| 247 | + /// # fn cert() -> reqwest::Certificate { unimplemented!() } |
| 248 | + /// let cert = cert(); |
| 249 | + /// // You can capture arbitrary configuration here (certs, proxies, etc.) |
| 250 | + /// // and apply it to the `reqwest::ClientBuilder` on platforms that support it. |
| 251 | + /// let node = NodeConfig::new("https://secure.example.com") |
| 252 | + /// .http_builder(move |builder| { |
| 253 | + /// builder |
| 254 | + /// .add_root_certificate(cert) |
| 255 | + /// .connect_timeout(std::time::Duration::from_secs(10)) |
| 256 | + /// }); |
| 257 | + /// } |
| 258 | + /// ``` |
| 259 | + /// |
| 260 | + /// # Multiple nodes with the same configuration |
| 261 | + /// |
| 262 | + /// The closure is `FnOnce`, so it is consumed when the HTTP client for that node |
| 263 | + /// is built. To use the same configuration (e.g. the same TLS certificate) for |
| 264 | + /// multiple nodes, clone the value once per node when building the configs: |
| 265 | + /// |
| 266 | + /// ```no_run |
| 267 | + /// #[cfg(not(target_family = "wasm"))] |
| 268 | + /// { |
| 269 | + /// use typesense::{Client, NodeConfig}; |
| 270 | + /// |
| 271 | + /// # fn cert() -> reqwest::Certificate { unimplemented!() } |
| 272 | + /// let cert = cert(); |
| 273 | + /// let nodes = ["https://node1:8108", "https://node2:8108"] |
| 274 | + /// .into_iter() |
| 275 | + /// .map(|url| { |
| 276 | + /// let cert_for_node = cert.clone(); |
| 277 | + /// NodeConfig::new(url).http_builder(move |b| { |
| 278 | + /// b.add_root_certificate(cert_for_node) |
| 279 | + /// }) |
| 280 | + /// }) |
| 281 | + /// .collect::<Vec<_>>(); |
| 282 | + /// let _client = Client::builder().nodes(nodes).api_key("key").build(); |
| 283 | + /// } |
| 284 | + /// ``` |
| 285 | + pub fn http_builder( |
| 286 | + mut self, |
| 287 | + f: impl FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder + 'static, |
| 288 | + ) -> Self { |
| 289 | + self.http_builder = Some(Box::new(f)); |
| 290 | + self |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +impl From<String> for NodeConfig { |
| 295 | + fn from(url: String) -> Self { |
| 296 | + Self::new(url) |
| 297 | + } |
| 298 | +} |
| 299 | + |
| 300 | +impl<'a> From<&'a str> for NodeConfig { |
| 301 | + fn from(url: &'a str) -> Self { |
| 302 | + Self::new(url) |
| 303 | + } |
| 304 | +} |
| 305 | + |
| 306 | +impl From<reqwest::Url> for NodeConfig { |
| 307 | + fn from(url: reqwest::Url) -> Self { |
| 308 | + Self::new(url) |
| 309 | + } |
| 310 | +} |
| 311 | + |
185 | 312 | // This is an internal detail to track the state of each node. |
186 | 313 | #[derive(Debug)] |
187 | 314 | struct Node { |
@@ -219,54 +346,64 @@ impl Client { |
219 | 346 | /// - **nearest_node**: None. |
220 | 347 | /// - **healthcheck_interval**: 60 seconds. |
221 | 348 | /// - **retry_policy**: Exponential backoff with a maximum of 3 retries. (disabled on WASM) |
222 | | - /// - **connection_timeout**: 5 seconds. (disabled on WASM) |
| 349 | + /// - **http_builder**: An `FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder` closure |
| 350 | + /// for per-node HTTP client customization (optional, via [`NodeConfig`]). |
| 351 | + /// |
| 352 | + /// When no custom `http_builder` is configured, a default `reqwest::ClientBuilder` with |
| 353 | + /// a 5-second connect timeout is used (native targets only). |
223 | 354 | #[builder] |
224 | 355 | pub fn new( |
225 | 356 | /// The Typesense API key used for authentication. |
226 | 357 | #[builder(into)] |
227 | 358 | api_key: String, |
228 | 359 | /// A list of all nodes in the Typesense cluster. |
| 360 | + /// |
| 361 | + /// Accepts plain URL strings or [`NodeConfig`] instances for per-node |
| 362 | + /// HTTP client customization. |
229 | 363 | #[builder( |
230 | | - with = |iter: impl IntoIterator<Item = impl Into<String>>| |
231 | | - iter.into_iter().map(Into::into).collect::<Vec<String>>() |
| 364 | + with = |iter: impl IntoIterator<Item = impl Into<NodeConfig>>| |
| 365 | + iter.into_iter().map(Into::into).collect::<Vec<NodeConfig>>() |
232 | 366 | )] |
233 | | - nodes: Vec<String>, |
| 367 | + nodes: Vec<NodeConfig>, |
234 | 368 | #[builder(into)] |
235 | 369 | /// An optional, preferred node to try first for every request. |
236 | 370 | /// This is for your server-side load balancer. |
237 | 371 | /// Do not add this node to all nodes list, should be a separate one. |
238 | | - nearest_node: Option<String>, |
| 372 | + nearest_node: Option<NodeConfig>, |
239 | 373 | #[builder(default = Duration::from_secs(60))] |
240 | 374 | /// The duration after which an unhealthy node will be retried for requests. |
241 | 375 | healthcheck_interval: Duration, |
242 | 376 | #[builder(default = ExponentialBackoff::builder().build_with_max_retries(3))] |
243 | 377 | /// The retry policy for transient network errors on a *single* node. |
244 | 378 | retry_policy: ExponentialBackoff, |
245 | | - #[builder(default = Duration::from_secs(5))] |
246 | | - /// The timeout for each individual network request. |
247 | | - connection_timeout: Duration, |
248 | 379 | ) -> Result<Self, &'static str> { |
249 | 380 | let is_nearest_node_set = nearest_node.is_some(); |
250 | 381 |
|
251 | 382 | let nodes: Vec<_> = nodes |
252 | 383 | .into_iter() |
253 | 384 | .chain(nearest_node) |
254 | | - .map(|mut url| { |
| 385 | + .map(|node_config| { |
| 386 | + let builder = match node_config.http_builder { |
| 387 | + Some(f) => f(reqwest::Client::builder()), |
| 388 | + None => { |
| 389 | + let b = reqwest::Client::builder(); |
| 390 | + #[cfg(not(target_arch = "wasm32"))] |
| 391 | + let b = b.connect_timeout(Duration::from_secs(5)); |
| 392 | + b |
| 393 | + } |
| 394 | + }; |
| 395 | + |
255 | 396 | #[cfg(target_arch = "wasm32")] |
256 | | - let http_client = reqwest::Client::builder() |
257 | | - .build() |
258 | | - .expect("Failed to build reqwest client"); |
| 397 | + let http_client = builder.build().expect("Failed to build reqwest client"); |
259 | 398 |
|
260 | 399 | #[cfg(not(target_arch = "wasm32"))] |
261 | 400 | let http_client = ReqwestMiddlewareClientBuilder::new( |
262 | | - reqwest::Client::builder() |
263 | | - .timeout(connection_timeout) |
264 | | - .build() |
265 | | - .expect("Failed to build reqwest client"), |
| 401 | + builder.build().expect("Failed to build reqwest client"), |
266 | 402 | ) |
267 | 403 | .with(RetryTransientMiddleware::new_with_policy(retry_policy)) |
268 | 404 | .build(); |
269 | 405 |
|
| 406 | + let mut url = node_config.url; |
270 | 407 | if url.len() > 1 && matches!(url.chars().last(), Some('/')) { |
271 | 408 | url.pop(); |
272 | 409 | } |
|
0 commit comments