1use std::path::Path;
2use std::time::Duration;
3
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7use crate::errors::{KodeBridgeError, Result};
8use crate::http_client::{send_request, RequestBuilder, Response};
9use crate::metrics::global_metrics;
10use crate::pool::{ConnectionPool, PoolConfig, PooledConnection};
11use crate::retry::{RetryConfig, RetryExecutor};
12#[cfg(windows)]
13use crate::transport::WindowsServerVerification;
14use crate::transport::{Endpoint, IpcStream};
15use bytes::Bytes;
16use http::Method;
17use std::str::FromStr as _;
18use tracing::{debug, trace};
19
20#[derive(Debug, Clone)]
22pub struct ClientConfig {
23 pub default_timeout: Duration,
25 pub pool_config: PoolConfig,
27 pub enable_pooling: bool,
29 pub max_retries: usize,
31 pub retry_delay: Duration,
32 pub max_concurrent_requests: usize,
34 pub max_requests_per_second: Option<f64>,
36 pub require_windows_server_system: bool,
40 #[cfg(windows)]
45 pub windows_server_pid_verifier: Option<fn(u32) -> std::io::Result<()>>,
46}
47
48impl Default for ClientConfig {
49 fn default() -> Self {
50 Self {
51 default_timeout: Duration::from_secs(5), pool_config: PoolConfig::default(),
53 enable_pooling: true,
54 max_retries: 3, retry_delay: Duration::from_millis(25), max_concurrent_requests: 16, max_requests_per_second: Some(50.0), require_windows_server_system: false,
59 #[cfg(windows)]
60 windows_server_pid_verifier: None,
61 }
62 }
63}
64
65pub struct IpcHttpClient {
70 endpoint: Endpoint,
71 config: ClientConfig,
72 pool: Option<ConnectionPool>,
73 retry_executor: RetryExecutor,
74 put_retry_executor: RetryExecutor,
76}
77
78pub struct HttpRequestBuilder<'a> {
80 client: &'a IpcHttpClient,
81 method: Method,
82 path: String,
83 body: Option<RequestBody>,
84 timeout: Option<Duration>,
85 headers: Vec<(String, String)>,
86 put_optimized: bool,
88 expected_size: Option<usize>,
90}
91
92enum RequestBody {
93 Json(Value),
94 JsonBytes(Bytes),
95}
96
97#[derive(Debug)]
99pub struct HttpResponse {
100 inner: Response,
101}
102
103impl HttpResponse {
104 const fn new(response: Response) -> Self {
105 Self { inner: response }
106 }
107
108 pub const fn status(&self) -> u16 {
110 self.inner.status_code()
111 }
112
113 pub fn headers(&self) -> Value {
115 let headers_map: std::collections::HashMap<String, String> = self
116 .inner
117 .headers()
118 .iter()
119 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
120 .collect();
121 serde_json::to_value(headers_map).unwrap_or(Value::Null)
122 }
123
124 pub fn body(&self) -> Result<String> {
126 self.inner.text()
127 }
128
129 pub fn is_success(&self) -> bool {
131 self.inner.is_success()
132 }
133
134 pub fn is_client_error(&self) -> bool {
136 self.inner.is_client_error()
137 }
138
139 pub fn is_server_error(&self) -> bool {
141 self.inner.is_server_error()
142 }
143
144 pub fn content_length(&self) -> u64 {
146 self.inner.content_length().unwrap_or(0)
147 }
148
149 pub fn json<T>(&self) -> Result<T>
151 where
152 T: DeserializeOwned,
153 {
154 self.inner.json()
155 }
156
157 pub fn json_value(&self) -> Result<Value> {
159 self.inner.json_value()
160 }
161
162 pub fn into_inner(self) -> Response {
164 self.inner
165 }
166
167 pub fn to_legacy(&self) -> crate::response::LegacyResponse {
169 self.inner.to_legacy()
170 }
171}
172
173impl IpcHttpClient {
174 pub fn new<P>(path: P) -> Result<Self>
176 where
177 P: AsRef<Path>,
178 {
179 Self::with_config(path, ClientConfig::default())
180 }
181
182 pub fn with_config<P>(path: P, config: ClientConfig) -> Result<Self>
184 where
185 P: AsRef<Path>,
186 {
187 let endpoint = Endpoint::new(path)?;
188
189 #[cfg(windows)]
190 let verification =
191 WindowsServerVerification::new(config.require_windows_server_system, config.windows_server_pid_verifier);
192 #[cfg(windows)]
193 let pool = config.enable_pooling.then(|| {
194 ConnectionPool::new_with_windows_server_verification(
195 endpoint.clone(),
196 config.pool_config.clone(),
197 verification,
198 )
199 });
200 #[cfg(not(windows))]
201 let pool = config
202 .enable_pooling
203 .then(|| ConnectionPool::new(endpoint.clone(), config.pool_config.clone()));
204
205 let retry_config = RetryConfig::for_network_operations()
207 .max_attempts(config.max_retries)
208 .base_delay(Duration::from_millis(config.pool_config.retry_delay_ms));
209
210 let retry_executor = RetryExecutor::new(retry_config);
211
212 let put_retry_config = RetryConfig::for_put_requests();
214 let put_retry_executor = RetryExecutor::new(put_retry_config);
215
216 Ok(Self {
217 endpoint,
218 config,
219 pool,
220 retry_executor,
221 put_retry_executor,
222 })
223 }
224
225 async fn connect_physical(&self) -> std::io::Result<IpcStream> {
226 #[cfg(windows)]
227 {
228 IpcStream::connect_with_windows_server_verification(
229 &self.endpoint,
230 WindowsServerVerification::new(
231 self.config.require_windows_server_system,
232 self.config.windows_server_pid_verifier,
233 ),
234 )
235 .await
236 }
237 #[cfg(not(windows))]
238 {
239 IpcStream::connect(&self.endpoint).await
240 }
241 }
242
243 async fn create_direct_connection(&self) -> Result<IpcStream> {
245 let mut last_error = None;
246
247 for attempt in 0..self.config.max_retries {
248 if attempt > 0 {
249 tokio::time::sleep(self.config.retry_delay).await;
250 }
251
252 match self.connect_physical().await {
253 Ok(stream) => {
254 debug!("Created direct connection on attempt {}", attempt + 1);
255 return Ok(stream);
256 }
257 Err(e) => {
258 trace!("Connection attempt {} failed: {}", attempt + 1, e);
259 last_error = Some(e);
260 }
261 }
262 }
263
264 Err(KodeBridgeError::connection(format!(
265 "Failed to create connection after {} attempts: {}",
266 self.config.max_retries,
267 last_error
268 .map(|e| e.to_string())
269 .unwrap_or_else(|| "Unknown error".to_string())
270 )))
271 }
272
273 async fn get_connection(&self) -> Result<Either<PooledConnection, IpcStream>> {
275 let metrics = global_metrics();
276
277 if let Some(ref pool) = self.pool {
278 match pool.get_connection().await {
279 Ok(conn) => {
280 metrics.connection_created(true); Ok(Either::Pool(conn))
282 }
283 Err(e) => {
284 metrics.connection_failed();
285 Err(e)
286 }
287 }
288 } else {
289 match self.create_direct_connection().await {
290 Ok(stream) => {
291 metrics.connection_created(false); Ok(Either::Direct(stream))
293 }
294 Err(e) => {
295 metrics.connection_failed();
296 Err(e)
297 }
298 }
299 }
300 }
301
302 pub async fn request(
304 &self,
305 method: &str,
306 path: &str,
307 body: Option<&serde_json::Value>,
308 ) -> crate::errors::AnyResult<crate::response::LegacyResponse> {
309 let response = self
310 .send_request_internal(method, path, body, self.config.default_timeout)
311 .await?;
312 Ok(response.to_legacy())
313 }
314
315 async fn send_request_internal(
317 &self,
318 method: &str,
319 path: &str,
320 body: Option<&Value>,
321 timeout: Duration,
322 ) -> Result<Response> {
323 let method =
324 Method::from_str(method).map_err(|e| KodeBridgeError::invalid_request(format!("Invalid method: {}", e)))?;
325
326 let mut builder = RequestBuilder::new(method.clone(), path.to_string());
327
328 if let Some(json_body) = body {
332 builder = builder.json(json_body)?;
333 }
334
335 let request = builder.build()?;
336
337 self.retry_executor
339 .execute_with_context(&format!("{} {}", method.as_str(), path), || async {
340 let result = tokio::time::timeout(timeout, async {
342 let mut connection = self.get_connection().await?;
343
344 match &mut connection {
345 Either::Pool(conn) => {
346 if let Some(stream) = conn.stream() {
347 let result = send_request(stream, request.clone()).await;
348 if result.is_err() {
349 conn.invalidate();
350 }
351 result
352 } else {
353 conn.invalidate();
354 Err(KodeBridgeError::connection("Pooled connection is invalid"))
355 }
356 }
357 Either::Direct(stream) => send_request(stream, request.clone()).await,
358 }
359 })
360 .await;
361
362 match result {
363 Ok(response) => response,
364 Err(_) => Err(KodeBridgeError::timeout(timeout.as_millis() as u64)),
365 }
366 })
367 .await
368 }
369
370 async fn send_request_with_optimization(
372 &self,
373 method: &str,
374 path: &str,
375 body: Option<&RequestBody>,
376 headers: &[(String, String)],
377 timeout: Duration,
378 is_put_optimized: bool,
379 expected_size: Option<usize>,
380 ) -> Result<Response> {
381 let method_enum =
382 Method::from_str(method).map_err(|e| KodeBridgeError::invalid_request(format!("Invalid method: {}", e)))?;
383
384 let mut builder = RequestBuilder::new(method_enum.clone(), path.to_string());
385
386 for (key, value) in headers {
388 builder = builder.header(key.as_str(), value.as_str());
389 }
390
391 if let Some(body) = body {
392 builder = match body {
393 RequestBody::Json(value) => builder.json(value)?,
394 RequestBody::JsonBytes(bytes) => builder.body_bytes(bytes.clone(), "application/json")?,
395 };
396 }
397
398 let request = builder.build()?;
399
400 let retry_context = if is_put_optimized {
402 format!("PUT_OPTIMIZED {}", path)
403 } else {
404 format!("{} {}", method, path)
405 };
406
407 let retry_executor = if is_put_optimized {
409 &self.put_retry_executor } else {
411 &self.retry_executor };
413
414 retry_executor
415 .execute_with_context(&retry_context, || async {
416 let result = tokio::time::timeout(timeout, async {
418 let mut connection = if is_put_optimized && expected_size.unwrap_or(0) > 10240 {
419 self.get_fresh_connection().await?
421 } else {
422 self.get_connection().await?
423 };
424
425 match &mut connection {
426 Either::Pool(conn) => {
427 if let Some(stream) = conn.stream() {
428 let result = send_request(stream, request.clone()).await;
429 if result.is_err() {
430 conn.invalidate();
431 }
432 result
433 } else {
434 conn.invalidate();
435 Err(KodeBridgeError::connection("Pooled connection is invalid"))
436 }
437 }
438 Either::Direct(stream) => send_request(stream, request.clone()).await,
439 }
440 })
441 .await;
442
443 match result {
444 Ok(response) => response,
445 Err(_) => Err(KodeBridgeError::timeout(timeout.as_millis() as u64)),
446 }
447 })
448 .await
449 }
450
451 async fn get_fresh_connection(&self) -> Result<Either<PooledConnection, IpcStream>> {
453 if let Some(ref pool) = self.pool {
455 match tokio::time::timeout(Duration::from_millis(20), pool.get_fresh_connection()).await {
456 Ok(Ok(conn)) => return Ok(Either::Pool(conn)),
457 Ok(Err(_)) | Err(_) => {
458 }
460 }
461 }
462
463 match tokio::time::timeout(Duration::from_millis(100), self.connect_physical()).await {
465 Ok(Ok(stream)) => Ok(Either::Direct(stream)),
466 Ok(Err(_)) | Err(_) => {
467 if let Some(ref pool) = self.pool {
469 let conn = pool.get_connection().await?;
470 Ok(Either::Pool(conn))
471 } else {
472 Err(KodeBridgeError::connection("Failed to get fresh connection"))
473 }
474 }
475 }
476 }
477
478 pub fn get(&self, path: &str) -> HttpRequestBuilder<'_> {
480 HttpRequestBuilder::new(self, Method::GET, path)
481 }
482
483 pub fn post(&self, path: &str) -> HttpRequestBuilder<'_> {
485 HttpRequestBuilder::new(self, Method::POST, path)
486 }
487
488 pub fn put(&self, path: &str) -> HttpRequestBuilder<'_> {
490 HttpRequestBuilder::new(self, Method::PUT, path)
491 }
492
493 pub async fn put_batch(&self, requests: Vec<(String, Value)>) -> Result<Vec<HttpResponse>> {
495 let batch_size = requests.len();
496 if batch_size == 0 {
497 return Ok(Vec::new());
498 }
499
500 let concurrent_limit = std::cmp::min(self.config.max_concurrent_requests, batch_size);
501 let mut responses = Vec::with_capacity(batch_size);
502 let mut pending = Vec::with_capacity(concurrent_limit);
503
504 for (path, body) in requests {
505 let body = Bytes::from(body.to_string());
506 pending.push(self.put(&path).json_bytes(body).optimize_for_put().send());
507
508 if pending.len() == concurrent_limit {
509 let chunk_results = futures::future::join_all(std::mem::take(&mut pending)).await;
510 for result in chunk_results {
511 match result {
512 Ok(response) => responses.push(response),
513 Err(e) => return Err(e),
514 }
515 }
516 }
517 }
518
519 if !pending.is_empty() {
520 let chunk_results = futures::future::join_all(pending).await;
521 for result in chunk_results {
522 match result {
523 Ok(response) => responses.push(response),
524 Err(e) => return Err(e),
525 }
526 }
527 }
528
529 Ok(responses)
530 }
531
532 pub fn delete(&self, path: &str) -> HttpRequestBuilder<'_> {
534 HttpRequestBuilder::new(self, Method::DELETE, path)
535 }
536
537 pub fn patch(&self, path: &str) -> HttpRequestBuilder<'_> {
539 HttpRequestBuilder::new(self, Method::PATCH, path)
540 }
541
542 pub fn head(&self, path: &str) -> HttpRequestBuilder<'_> {
544 HttpRequestBuilder::new(self, Method::HEAD, path)
545 }
546
547 pub fn options(&self, path: &str) -> HttpRequestBuilder<'_> {
549 HttpRequestBuilder::new(self, Method::OPTIONS, path)
550 }
551
552 pub fn pool_stats(&self) -> Option<crate::pool::PoolStats> {
554 self.pool.as_ref().map(|p| p.stats())
555 }
556
557 pub fn close(&self) {
559 if let Some(ref pool) = self.pool {
560 pool.close();
561 }
562 }
563
564 pub async fn preheat_for_puts(&self, count: usize) {
566 if let Some(ref pool) = self.pool {
567 pool.preheat_for_puts(count).await;
568 }
569 }
570
571 fn calculate_smart_timeout(&self, method: &str, body_size: Option<usize>) -> Duration {
573 match method {
574 "PUT" | "POST" => {
575 match body_size {
576 Some(size) if size > 5 * 1024 * 1024 => Duration::from_secs(30), Some(size) if size > 1024 * 1024 => Duration::from_secs(15), Some(size) if size > 100 * 1024 => Duration::from_secs(8), Some(size) if size > 10 * 1024 => Duration::from_secs(4), _ => Duration::from_secs(2), }
582 }
583 _ => self.config.default_timeout, }
585 }
586}
587
588impl<'a> HttpRequestBuilder<'a> {
589 fn new(client: &'a IpcHttpClient, method: Method, path: &str) -> Self {
590 let is_put = method == Method::PUT;
591 Self {
592 client,
593 method,
594 path: path.to_string(),
595 body: None,
596 timeout: None,
597 headers: Vec::new(),
598 put_optimized: is_put, expected_size: None,
600 }
601 }
602
603 pub fn json_body(mut self, body: &Value) -> Self {
605 self.body = Some(RequestBody::Json(body.clone()));
606 self
607 }
608
609 pub fn json_bytes(mut self, body: Bytes) -> Self {
611 self.expected_size = Some(body.len());
612 self.body = Some(RequestBody::JsonBytes(body));
613 self
614 }
615
616 pub const fn timeout(mut self, timeout: Duration) -> Self {
618 self.timeout = Some(timeout);
619 self
620 }
621
622 pub const fn expected_size(mut self, size: usize) -> Self {
624 self.expected_size = Some(size);
625 self
626 }
627
628 pub const fn optimize_for_put(mut self) -> Self {
630 self.put_optimized = true;
631 self
632 }
633
634 pub fn header<K, V>(mut self, key: K, value: V) -> Self
636 where
637 K: Into<String>,
638 V: Into<String>,
639 {
640 self.headers.push((key.into(), value.into()));
641 self
642 }
643
644 pub async fn send(self) -> Result<HttpResponse> {
646 let metrics = global_metrics();
647 let tracker = metrics.request_start(self.method.as_str());
648
649 let timeout = if self.put_optimized {
651 self.timeout.unwrap_or_else(|| {
652 self.client
653 .calculate_smart_timeout(self.method.as_str(), self.expected_size)
654 })
655 } else {
656 self.timeout.unwrap_or(self.client.config.default_timeout)
657 };
658
659 match self
660 .client
661 .send_request_with_optimization(
662 self.method.as_str(),
663 &self.path,
664 self.body.as_ref(),
665 &self.headers,
666 timeout,
667 self.put_optimized,
668 self.expected_size,
669 )
670 .await
671 {
672 Ok(response) => {
673 tracker.success(response.status_code());
674 Ok(HttpResponse::new(response))
675 }
676 Err(e) => {
677 tracker.failure(&format!("{:?}", e));
678 Err(e)
679 }
680 }
681 }
682}
683
684enum Either<A, B> {
686 Pool(A),
687 Direct(B),
688}
689
690impl Drop for IpcHttpClient {
691 fn drop(&mut self) {
692 self.close();
693 }
694}