1use std::collections::HashMap;
6use std::hash::{Hash, Hasher};
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::time::Duration;
10
11use parking_lot::RwLock;
12use reqwest::{Certificate, Client, Identity};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig, CircuitState};
17use barbacane_plugin_sdk::types::base64_body;
18
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct TlsConfig {
22 #[serde(default)]
24 pub client_cert: Option<PathBuf>,
25 #[serde(default)]
27 pub client_key: Option<PathBuf>,
28 #[serde(default)]
30 pub ca: Option<PathBuf>,
31}
32
33impl TlsConfig {
34 pub fn is_configured(&self) -> bool {
36 self.client_cert.is_some() || self.client_key.is_some() || self.ca.is_some()
37 }
38
39 pub fn validate(&self) -> Result<(), TlsConfigError> {
41 match (&self.client_cert, &self.client_key) {
42 (Some(_), None) => Err(TlsConfigError::MissingClientKey),
43 (None, Some(_)) => Err(TlsConfigError::MissingClientCert),
44 _ => Ok(()),
45 }
46 }
47
48 fn cache_key(&self) -> TlsCacheKey {
50 TlsCacheKey {
51 client_cert: self.client_cert.clone(),
52 client_key: self.client_key.clone(),
53 ca: self.ca.clone(),
54 }
55 }
56}
57
58#[derive(Debug, Error)]
60pub enum TlsConfigError {
61 #[error("client_cert specified but client_key is missing")]
62 MissingClientKey,
63 #[error("client_key specified but client_cert is missing")]
64 MissingClientCert,
65 #[error("failed to read certificate file: {0}")]
66 ReadCertificate(#[source] std::io::Error),
67 #[error("failed to read key file: {0}")]
68 ReadKey(#[source] std::io::Error),
69 #[error("failed to read CA file: {0}")]
70 ReadCa(#[source] std::io::Error),
71 #[error("failed to parse PEM identity: {0}")]
72 ParseIdentity(#[source] reqwest::Error),
73 #[error("failed to parse CA certificate: {0}")]
74 ParseCaCert(#[source] reqwest::Error),
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79struct TlsCacheKey {
80 client_cert: Option<PathBuf>,
81 client_key: Option<PathBuf>,
82 ca: Option<PathBuf>,
83}
84
85impl Hash for TlsCacheKey {
86 fn hash<H: Hasher>(&self, state: &mut H) {
87 self.client_cert.hash(state);
88 self.client_key.hash(state);
89 self.ca.hash(state);
90 }
91}
92
93#[derive(Clone)]
95pub struct HttpClient {
96 client: Client,
98 tls_clients: Arc<RwLock<HashMap<TlsCacheKey, Client>>>,
100 base_config: HttpClientConfig,
102 circuit_breakers: Arc<RwLock<HashMap<String, CircuitBreaker>>>,
103 default_timeout: Duration,
104 allow_plaintext: bool,
105}
106
107impl HttpClient {
108 pub fn new(config: HttpClientConfig) -> Result<Self, HttpClientError> {
110 let client = Client::builder()
111 .pool_max_idle_per_host(config.pool_max_idle_per_host)
112 .pool_idle_timeout(config.pool_idle_timeout)
113 .connect_timeout(config.connect_timeout)
114 .timeout(config.default_timeout)
115 .build()
116 .map_err(HttpClientError::BuildError)?;
117
118 let default_timeout = config.default_timeout;
119 let allow_plaintext = config.allow_plaintext;
120
121 Ok(Self {
122 client,
123 tls_clients: Arc::new(RwLock::new(HashMap::new())),
124 base_config: config,
125 circuit_breakers: Arc::new(RwLock::new(HashMap::new())),
126 default_timeout,
127 allow_plaintext,
128 })
129 }
130
131 fn get_or_create_tls_client(&self, tls_config: &TlsConfig) -> Result<Client, HttpClientError> {
133 let cache_key = tls_config.cache_key();
134
135 {
137 let clients = self.tls_clients.read();
138 if let Some(client) = clients.get(&cache_key) {
139 return Ok(client.clone());
140 }
141 }
142
143 let client = self.build_tls_client(tls_config)?;
145
146 {
148 let mut clients = self.tls_clients.write();
149 clients.insert(cache_key, client.clone());
150 }
151
152 Ok(client)
153 }
154
155 fn build_tls_client(&self, tls_config: &TlsConfig) -> Result<Client, HttpClientError> {
157 tls_config.validate().map_err(HttpClientError::TlsConfig)?;
158
159 let mut builder = Client::builder()
160 .pool_max_idle_per_host(self.base_config.pool_max_idle_per_host)
161 .pool_idle_timeout(self.base_config.pool_idle_timeout)
162 .connect_timeout(self.base_config.connect_timeout)
163 .timeout(self.base_config.default_timeout);
164
165 if let (Some(cert_path), Some(key_path)) = (&tls_config.client_cert, &tls_config.client_key)
167 {
168 let cert_pem = std::fs::read(cert_path)
169 .map_err(|e| HttpClientError::TlsConfig(TlsConfigError::ReadCertificate(e)))?;
170 let key_pem = std::fs::read(key_path)
171 .map_err(|e| HttpClientError::TlsConfig(TlsConfigError::ReadKey(e)))?;
172
173 let mut pem = cert_pem;
175 pem.extend_from_slice(&key_pem);
176
177 let identity = Identity::from_pem(&pem)
178 .map_err(|e| HttpClientError::TlsConfig(TlsConfigError::ParseIdentity(e)))?;
179
180 builder = builder.identity(identity);
181 }
182
183 if let Some(ca_path) = &tls_config.ca {
185 let ca_pem = std::fs::read(ca_path)
186 .map_err(|e| HttpClientError::TlsConfig(TlsConfigError::ReadCa(e)))?;
187
188 let ca_cert = Certificate::from_pem(&ca_pem)
189 .map_err(|e| HttpClientError::TlsConfig(TlsConfigError::ParseCaCert(e)))?;
190
191 builder = builder.add_root_certificate(ca_cert);
192 }
193
194 builder.build().map_err(HttpClientError::BuildError)
195 }
196
197 pub async fn call(&self, request: HttpRequest) -> Result<HttpResponse, HttpClientError> {
199 self.call_with_tls(request, None).await
200 }
201
202 pub async fn stream_raw(
211 &self,
212 request: HttpRequest,
213 ) -> Result<reqwest::Response, HttpClientError> {
214 let url = request
215 .url
216 .parse::<reqwest::Url>()
217 .map_err(|e| HttpClientError::InvalidUrl(e.to_string()))?;
218
219 if url.scheme() == "http" && !self.allow_plaintext {
220 return Err(HttpClientError::PlaintextNotAllowed);
221 }
222
223 let host = url
224 .host_str()
225 .ok_or_else(|| HttpClientError::InvalidUrl("missing host".into()))?
226 .to_string();
227
228 let circuit_state = self.get_circuit_state(&host);
229 if circuit_state == crate::circuit_breaker::CircuitState::Open {
230 return Err(HttpClientError::CircuitOpen(host));
231 }
232
233 let method = request
234 .method
235 .parse::<reqwest::Method>()
236 .map_err(|e| HttpClientError::InvalidMethod(e.to_string()))?;
237
238 let timeout = request.timeout.unwrap_or(self.default_timeout);
239
240 let mut req_builder = self.client.request(method, url).timeout(timeout);
241
242 for (key, value) in &request.headers {
243 req_builder = req_builder.header(key.as_str(), value.as_str());
244 }
245
246 if let Some(body) = request.body {
247 req_builder = req_builder.body(body);
248 }
249
250 match req_builder.send().await {
251 Ok(response) => Ok(response),
252 Err(e) => {
253 self.record_failure(&host);
254 if e.is_timeout() {
255 Err(HttpClientError::Timeout)
256 } else if e.is_connect() {
257 Err(HttpClientError::ConnectionFailed(e.to_string()))
258 } else {
259 Err(HttpClientError::RequestFailed(e.to_string()))
260 }
261 }
262 }
263 }
264
265 pub async fn call_with_tls(
267 &self,
268 request: HttpRequest,
269 tls_config: Option<&TlsConfig>,
270 ) -> Result<HttpResponse, HttpClientError> {
271 let url = request
273 .url
274 .parse::<reqwest::Url>()
275 .map_err(|e| HttpClientError::InvalidUrl(e.to_string()))?;
276
277 if url.scheme() == "http" && !self.allow_plaintext {
278 return Err(HttpClientError::PlaintextNotAllowed);
279 }
280
281 let host = url
283 .host_str()
284 .ok_or_else(|| HttpClientError::InvalidUrl("missing host".into()))?
285 .to_string();
286
287 let circuit_state = self.get_circuit_state(&host);
289 if circuit_state == CircuitState::Open {
290 return Err(HttpClientError::CircuitOpen(host));
291 }
292
293 let client = match tls_config {
295 Some(tls) if tls.is_configured() => self.get_or_create_tls_client(tls)?,
296 _ => self.client.clone(),
297 };
298
299 let method = request
301 .method
302 .parse::<reqwest::Method>()
303 .map_err(|e| HttpClientError::InvalidMethod(e.to_string()))?;
304
305 let timeout = request.timeout.unwrap_or(self.default_timeout);
306
307 let mut req_builder = client.request(method, url).timeout(timeout);
308
309 for (key, value) in &request.headers {
311 req_builder = req_builder.header(key.as_str(), value.as_str());
312 }
313
314 if let Some(body) = request.body {
316 req_builder = req_builder.body(body);
317 }
318
319 let result = req_builder.send().await;
321
322 match result {
323 Ok(response) => {
324 self.record_success(&host);
326
327 let status = response.status().as_u16();
328 let headers: HashMap<String, String> = response
329 .headers()
330 .iter()
331 .filter_map(|(k, v)| {
332 v.to_str()
333 .ok()
334 .map(|v| (k.as_str().to_lowercase(), v.to_string()))
335 })
336 .collect();
337
338 let body = response
339 .bytes()
340 .await
341 .map_err(HttpClientError::ResponseReadError)?;
342
343 Ok(HttpResponse {
344 status,
345 headers,
346 body: Some(body.to_vec()),
347 })
348 }
349 Err(e) => {
350 self.record_failure(&host);
352
353 if e.is_timeout() {
354 Err(HttpClientError::Timeout)
355 } else if e.is_connect() {
356 Err(HttpClientError::ConnectionFailed(e.to_string()))
357 } else {
358 Err(HttpClientError::RequestFailed(e.to_string()))
359 }
360 }
361 }
362 }
363
364 pub fn configure_circuit_breaker(&self, host: &str, config: CircuitBreakerConfig) {
366 let mut breakers = self.circuit_breakers.write();
367 breakers.insert(host.to_string(), CircuitBreaker::new(config));
368 }
369
370 fn get_circuit_state(&self, host: &str) -> CircuitState {
371 let breakers = self.circuit_breakers.read();
372 breakers
373 .get(host)
374 .map(|cb| cb.state())
375 .unwrap_or(CircuitState::Closed)
376 }
377
378 fn record_success(&self, host: &str) {
379 let mut breakers = self.circuit_breakers.write();
380 if let Some(cb) = breakers.get_mut(host) {
381 cb.record_success();
382 }
383 }
384
385 fn record_failure(&self, host: &str) {
386 let mut breakers = self.circuit_breakers.write();
387 if let Some(cb) = breakers.get_mut(host) {
388 cb.record_failure();
389 }
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct HttpClientConfig {
396 pub pool_max_idle_per_host: usize,
398 pub pool_idle_timeout: Duration,
400 pub connect_timeout: Duration,
402 pub default_timeout: Duration,
404 pub allow_plaintext: bool,
406}
407
408impl Default for HttpClientConfig {
409 fn default() -> Self {
410 Self {
411 pool_max_idle_per_host: 10,
412 pool_idle_timeout: Duration::from_secs(90),
413 connect_timeout: Duration::from_secs(10),
414 default_timeout: Duration::from_secs(30),
415 allow_plaintext: false,
416 }
417 }
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct HttpRequest {
423 pub method: String,
425 pub url: String,
427 #[serde(default)]
429 pub headers: HashMap<String, String>,
430 #[serde(default, with = "base64_body")]
432 pub body: Option<Vec<u8>>,
433 #[serde(default, with = "option_duration_serde")]
435 pub timeout: Option<Duration>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct HttpResponse {
441 pub status: u16,
443 pub headers: HashMap<String, String>,
445 #[serde(default, with = "base64_body")]
447 pub body: Option<Vec<u8>>,
448}
449
450impl HttpResponse {
451 pub fn error(status: u16, error_type: &str, title: &str, detail: &str) -> Self {
453 let body = serde_json::json!({
454 "type": error_type,
455 "title": title,
456 "status": status,
457 "detail": detail
458 });
459
460 let mut headers = HashMap::new();
461 headers.insert(
462 "content-type".to_string(),
463 "application/problem+json".to_string(),
464 );
465
466 Self {
467 status,
468 headers,
469 body: Some(body.to_string().into_bytes()),
470 }
471 }
472}
473
474#[derive(Debug, Error)]
476pub enum HttpClientError {
477 #[error("failed to build HTTP client: {0}")]
478 BuildError(#[source] reqwest::Error),
479
480 #[error("invalid URL: {0}")]
481 InvalidUrl(String),
482
483 #[error("invalid HTTP method: {0}")]
484 InvalidMethod(String),
485
486 #[error("plaintext HTTP not allowed")]
487 PlaintextNotAllowed,
488
489 #[error("circuit breaker open for host: {0}")]
490 CircuitOpen(String),
491
492 #[error("request timeout")]
493 Timeout,
494
495 #[error("connection failed: {0}")]
496 ConnectionFailed(String),
497
498 #[error("request failed: {0}")]
499 RequestFailed(String),
500
501 #[error("failed to read response: {0}")]
502 ResponseReadError(#[source] reqwest::Error),
503
504 #[error("TLS configuration error: {0}")]
505 TlsConfig(#[source] TlsConfigError),
506}
507
508mod option_duration_serde {
510 use serde::{Deserialize, Deserializer, Serialize, Serializer};
511 use std::time::Duration;
512
513 pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
514 where
515 S: Serializer,
516 {
517 match duration {
518 Some(d) => d.as_secs_f64().serialize(serializer),
519 None => serializer.serialize_none(),
520 }
521 }
522
523 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
524 where
525 D: Deserializer<'de>,
526 {
527 let opt: Option<f64> = Option::deserialize(deserializer)?;
528 Ok(opt.map(Duration::from_secs_f64))
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_config_default() {
538 let config = HttpClientConfig::default();
539 assert_eq!(config.pool_max_idle_per_host, 10);
540 assert_eq!(config.default_timeout, Duration::from_secs(30));
541 assert!(!config.allow_plaintext);
542 }
543
544 #[test]
545 fn test_error_response() {
546 let resp = HttpResponse::error(
547 502,
548 "urn:barbacane:error:upstream-unavailable",
549 "Bad Gateway",
550 "Failed to connect to upstream",
551 );
552
553 assert_eq!(resp.status, 502);
554 assert_eq!(
555 resp.headers.get("content-type"),
556 Some(&"application/problem+json".to_string())
557 );
558 }
559
560 #[test]
561 fn test_tls_config_default() {
562 let tls = TlsConfig::default();
563 assert!(tls.client_cert.is_none());
564 assert!(tls.client_key.is_none());
565 assert!(tls.ca.is_none());
566 assert!(!tls.is_configured());
567 }
568
569 #[test]
570 fn test_tls_config_is_configured() {
571 let mut tls = TlsConfig::default();
572 assert!(!tls.is_configured());
573
574 tls.client_cert = Some(PathBuf::from("/path/to/cert.pem"));
575 assert!(tls.is_configured());
576
577 tls.client_cert = None;
578 tls.ca = Some(PathBuf::from("/path/to/ca.pem"));
579 assert!(tls.is_configured());
580 }
581
582 #[test]
583 fn test_tls_config_validate_success() {
584 let tls = TlsConfig::default();
586 assert!(tls.validate().is_ok());
587
588 let tls = TlsConfig {
590 client_cert: None,
591 client_key: None,
592 ca: Some(PathBuf::from("/path/to/ca.pem")),
593 };
594 assert!(tls.validate().is_ok());
595
596 let tls = TlsConfig {
598 client_cert: Some(PathBuf::from("/path/to/cert.pem")),
599 client_key: Some(PathBuf::from("/path/to/key.pem")),
600 ca: None,
601 };
602 assert!(tls.validate().is_ok());
603 }
604
605 #[test]
606 fn test_tls_config_validate_missing_key() {
607 let tls = TlsConfig {
608 client_cert: Some(PathBuf::from("/path/to/cert.pem")),
609 client_key: None,
610 ca: None,
611 };
612 let err = tls.validate().unwrap_err();
613 assert!(matches!(err, TlsConfigError::MissingClientKey));
614 }
615
616 #[test]
617 fn test_tls_config_validate_missing_cert() {
618 let tls = TlsConfig {
619 client_cert: None,
620 client_key: Some(PathBuf::from("/path/to/key.pem")),
621 ca: None,
622 };
623 let err = tls.validate().unwrap_err();
624 assert!(matches!(err, TlsConfigError::MissingClientCert));
625 }
626
627 #[test]
628 fn test_tls_config_serde() {
629 let json = r#"{
630 "client_cert": "/etc/certs/client.crt",
631 "client_key": "/etc/certs/client.key",
632 "ca": "/etc/certs/ca.crt"
633 }"#;
634
635 let tls: TlsConfig = serde_json::from_str(json).unwrap();
636 assert_eq!(
637 tls.client_cert,
638 Some(PathBuf::from("/etc/certs/client.crt"))
639 );
640 assert_eq!(tls.client_key, Some(PathBuf::from("/etc/certs/client.key")));
641 assert_eq!(tls.ca, Some(PathBuf::from("/etc/certs/ca.crt")));
642 }
643
644 #[test]
645 fn test_tls_config_serde_partial() {
646 let json = r#"{"ca": "/etc/certs/ca.crt"}"#;
647
648 let tls: TlsConfig = serde_json::from_str(json).unwrap();
649 assert!(tls.client_cert.is_none());
650 assert!(tls.client_key.is_none());
651 assert_eq!(tls.ca, Some(PathBuf::from("/etc/certs/ca.crt")));
652 }
653
654 #[tokio::test]
657 async fn stream_raw_rejects_invalid_url() {
658 let client = HttpClient::new(HttpClientConfig::default()).expect("client");
659 let req = HttpRequest {
660 method: "GET".into(),
661 url: "not a url".into(),
662 headers: Default::default(),
663 body: None,
664 timeout: None,
665 };
666 assert!(matches!(
667 client.stream_raw(req).await,
668 Err(HttpClientError::InvalidUrl(_))
669 ));
670 }
671
672 #[tokio::test]
673 async fn stream_raw_rejects_plaintext_when_disallowed() {
674 let config = HttpClientConfig {
675 allow_plaintext: false,
676 ..Default::default()
677 };
678 let client = HttpClient::new(config).expect("client");
679 let req = HttpRequest {
680 method: "GET".into(),
681 url: "http://example.com/api".into(),
682 headers: Default::default(),
683 body: None,
684 timeout: None,
685 };
686 assert!(matches!(
687 client.stream_raw(req).await,
688 Err(HttpClientError::PlaintextNotAllowed)
689 ));
690 }
691
692 #[tokio::test]
693 async fn stream_raw_rejects_invalid_method() {
694 let config = HttpClientConfig {
695 allow_plaintext: true,
696 ..Default::default()
697 };
698 let client = HttpClient::new(config).expect("client");
699 let req = HttpRequest {
700 method: "NOT A METHOD!!!".into(),
701 url: "http://127.0.0.1:1/".into(),
702 headers: Default::default(),
703 body: None,
704 timeout: None,
705 };
706 assert!(matches!(
707 client.stream_raw(req).await,
708 Err(HttpClientError::InvalidMethod(_))
709 ));
710 }
711
712 #[tokio::test]
713 async fn stream_raw_connection_refused() {
714 let config = HttpClientConfig {
715 allow_plaintext: true,
716 ..Default::default()
717 };
718 let client = HttpClient::new(config).expect("client");
719 let req = HttpRequest {
720 method: "GET".into(),
721 url: "http://127.0.0.1:1/".into(), headers: Default::default(),
723 body: None,
724 timeout: None,
725 };
726 let err = client.stream_raw(req).await.unwrap_err();
727 assert!(
728 matches!(
729 err,
730 HttpClientError::ConnectionFailed(_) | HttpClientError::RequestFailed(_)
731 ),
732 "expected network error, got: {err:?}"
733 );
734 }
735
736 #[tokio::test]
737 async fn stream_raw_timeout() {
738 use tokio::net::TcpListener;
739
740 let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
742 let addr = listener.local_addr().expect("local_addr");
743
744 let config = HttpClientConfig {
745 allow_plaintext: true,
746 ..Default::default()
747 };
748 let client = HttpClient::new(config).expect("client");
749 let req = HttpRequest {
750 method: "GET".into(),
751 url: format!("http://{addr}/slow"),
752 headers: Default::default(),
753 body: None,
754 timeout: Some(Duration::from_millis(50)),
755 };
756 let err = client.stream_raw(req).await.unwrap_err();
757 assert!(
758 matches!(err, HttpClientError::Timeout),
759 "expected Timeout, got: {err:?}"
760 );
761
762 drop(listener);
763 }
764
765 #[tokio::test]
766 async fn stream_raw_successful_request() {
767 use tokio::io::AsyncWriteExt;
768 use tokio::net::TcpListener;
769
770 let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
771 let addr = listener.local_addr().expect("local_addr");
772
773 tokio::spawn(async move {
775 let (mut socket, _) = listener.accept().await.expect("accept");
776 let mut buf = [0u8; 1024];
777 let _ = tokio::io::AsyncReadExt::read(&mut socket, &mut buf).await;
778 let response = "HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok";
779 socket.write_all(response.as_bytes()).await.expect("write");
780 socket.shutdown().await.expect("shutdown");
781 });
782
783 let config = HttpClientConfig {
784 allow_plaintext: true,
785 ..Default::default()
786 };
787 let client = HttpClient::new(config).expect("client");
788 let req = HttpRequest {
789 method: "GET".into(),
790 url: format!("http://{addr}/"),
791 headers: Default::default(),
792 body: None,
793 timeout: None,
794 };
795 let resp = client
796 .stream_raw(req)
797 .await
798 .expect("stream_raw should succeed");
799 assert_eq!(resp.status(), 200);
800 let body = resp.text().await.expect("body");
801 assert_eq!(body, "ok");
802 }
803
804 #[test]
805 fn test_tls_cache_key_equality() {
806 let tls1 = TlsConfig {
807 client_cert: Some(PathBuf::from("/path/to/cert.pem")),
808 client_key: Some(PathBuf::from("/path/to/key.pem")),
809 ca: None,
810 };
811 let tls2 = TlsConfig {
812 client_cert: Some(PathBuf::from("/path/to/cert.pem")),
813 client_key: Some(PathBuf::from("/path/to/key.pem")),
814 ca: None,
815 };
816 let tls3 = TlsConfig {
817 client_cert: Some(PathBuf::from("/other/cert.pem")),
818 client_key: Some(PathBuf::from("/path/to/key.pem")),
819 ca: None,
820 };
821
822 assert_eq!(tls1.cache_key(), tls2.cache_key());
823 assert_ne!(tls1.cache_key(), tls3.cache_key());
824 }
825
826 #[test]
831 fn http_request_base64_body_roundtrip() {
832 let binary_body: Vec<u8> = vec![0x89, 0x50, 0x4E, 0x47, 0xFF, 0xFE, 0x00, 0x01];
833 let req = HttpRequest {
834 method: "POST".into(),
835 url: "https://example.com/upload".into(),
836 headers: Default::default(),
837 body: Some(binary_body.clone()),
838 timeout: None,
839 };
840
841 let json = serde_json::to_string(&req).unwrap();
842 assert!(
844 !json.contains("\\u0089"),
845 "body should be base64-encoded, not escaped unicode"
846 );
847
848 let decoded: HttpRequest = serde_json::from_str(&json).unwrap();
849 assert_eq!(decoded.body.unwrap(), binary_body);
850 }
851
852 #[test]
855 fn http_response_base64_body_roundtrip() {
856 let binary_body: Vec<u8> = vec![0x89, 0x50, 0x4E, 0x47, 0xFF, 0xFE, 0x00, 0x01];
857 let resp = HttpResponse {
858 status: 200,
859 headers: Default::default(),
860 body: Some(binary_body.clone()),
861 };
862
863 let json = serde_json::to_string(&resp).unwrap();
864 let decoded: HttpResponse = serde_json::from_str(&json).unwrap();
865 assert_eq!(decoded.body.unwrap(), binary_body);
866 }
867
868 #[test]
870 fn http_request_null_body_roundtrip() {
871 let req = HttpRequest {
872 method: "GET".into(),
873 url: "https://example.com".into(),
874 headers: Default::default(),
875 body: None,
876 timeout: None,
877 };
878
879 let json = serde_json::to_string(&req).unwrap();
880 assert!(json.contains(r#""body":null"#));
881
882 let decoded: HttpRequest = serde_json::from_str(&json).unwrap();
883 assert!(decoded.body.is_none());
884 }
885
886 #[test]
889 fn http_request_deserialize_from_plugin_json() {
890 use base64::Engine;
891 let raw_bytes: Vec<u8> = vec![0x00, 0x01, 0x80, 0xFF];
892 let b64 = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
893
894 let json = format!(
895 r#"{{
896 "method": "POST",
897 "url": "https://example.com/api",
898 "body": "{b64}"
899 }}"#
900 );
901
902 let req: HttpRequest = serde_json::from_str(&json).unwrap();
903 assert_eq!(req.body.unwrap(), raw_bytes);
904 }
905
906 #[test]
909 fn http_response_deserialize_from_host_json() {
910 use base64::Engine;
911 let raw_bytes: Vec<u8> = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A];
912 let b64 = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
913
914 let json = format!(
915 r#"{{
916 "status": 200,
917 "headers": {{}},
918 "body": "{b64}"
919 }}"#
920 );
921
922 let resp: HttpResponse = serde_json::from_str(&json).unwrap();
923 assert_eq!(resp.body.unwrap(), raw_bytes);
924 }
925}