1use crate::builder::HttpClientBuilder;
2use crate::config::TransportSecurity;
3use crate::error::HttpError;
4use crate::request::RequestBuilder;
5use crate::response::ResponseBody;
6use bytes::Bytes;
7use http::{Request, Response};
8use http_body_util::Full;
9use std::future::Future;
10use std::pin::Pin;
11use tower::Service;
12use tower::buffer::Buffer;
13
14pub type ServiceFuture =
16 Pin<Box<dyn Future<Output = Result<Response<ResponseBody>, HttpError>> + Send>>;
17
18pub type BufferedService = Buffer<Request<Full<Bytes>>, ServiceFuture>;
21
22#[derive(Clone)]
61pub struct HttpClient {
62 pub(crate) service: BufferedService,
64 pub(crate) max_body_size: usize,
66 pub(crate) transport_security: TransportSecurity,
68}
69
70impl HttpClient {
71 pub fn new() -> Result<Self, HttpError> {
76 HttpClientBuilder::new().build()
77 }
78
79 #[must_use]
81 pub fn builder() -> HttpClientBuilder {
82 HttpClientBuilder::new()
83 }
84
85 pub fn get(&self, url: &str) -> RequestBuilder {
137 RequestBuilder::new(
138 self.service.clone(),
139 self.max_body_size,
140 http::Method::GET,
141 url.to_owned(),
142 self.transport_security,
143 )
144 }
145
146 pub fn post(&self, url: &str) -> RequestBuilder {
176 RequestBuilder::new(
177 self.service.clone(),
178 self.max_body_size,
179 http::Method::POST,
180 url.to_owned(),
181 self.transport_security,
182 )
183 }
184
185 pub fn put(&self, url: &str) -> RequestBuilder {
207 RequestBuilder::new(
208 self.service.clone(),
209 self.max_body_size,
210 http::Method::PUT,
211 url.to_owned(),
212 self.transport_security,
213 )
214 }
215
216 pub fn patch(&self, url: &str) -> RequestBuilder {
238 RequestBuilder::new(
239 self.service.clone(),
240 self.max_body_size,
241 http::Method::PATCH,
242 url.to_owned(),
243 self.transport_security,
244 )
245 }
246
247 pub fn delete(&self, url: &str) -> RequestBuilder {
267 RequestBuilder::new(
268 self.service.clone(),
269 self.max_body_size,
270 http::Method::DELETE,
271 url.to_owned(),
272 self.transport_security,
273 )
274 }
275
276 pub fn head(&self, url: &str) -> RequestBuilder {
302 RequestBuilder::new(
303 self.service.clone(),
304 self.max_body_size,
305 http::Method::HEAD,
306 url.to_owned(),
307 self.transport_security,
308 )
309 }
310
311 pub fn options(&self, url: &str) -> RequestBuilder {
340 RequestBuilder::new(
341 self.service.clone(),
342 self.max_body_size,
343 http::Method::OPTIONS,
344 url.to_owned(),
345 self.transport_security,
346 )
347 }
348}
349
350pub fn map_buffer_error(err: tower::BoxError) -> HttpError {
355 match err.downcast::<HttpError>() {
357 Ok(http_err) => *http_err,
358 Err(err) => {
359 tracing::error!(
366 error = %err,
367 "buffer worker closed unexpectedly; service unavailable"
368 );
369 HttpError::ServiceClosed
370 }
371 }
372}
373
374pub async fn try_acquire_buffer_slot(service: &mut BufferedService) -> Result<(), HttpError> {
379 use std::task::Poll;
380
381 let poll_result = std::future::poll_fn(|cx| match service.poll_ready(cx) {
383 Poll::Ready(result) => Poll::Ready(Some(result)),
384 Poll::Pending => Poll::Ready(None), })
386 .await;
387
388 match poll_result {
389 Some(Ok(())) => Ok(()),
390 Some(Err(e)) => Err(map_buffer_error(e)),
391 None => Err(HttpError::Overloaded), }
393}
394
395#[cfg(test)]
408#[cfg(not(feature = "fips"))]
409#[cfg_attr(coverage_nightly, coverage(off))]
410mod tests {
411 use super::*;
412 use crate::error::HttpError;
413 use httpmock::prelude::*;
414 use serde_json::json;
415
416 fn test_client() -> HttpClient {
417 HttpClientBuilder::new().retry(None).build().unwrap()
418 }
419
420 #[tokio::test]
421 async fn test_http_client_get() {
422 let server = MockServer::start();
423 let _m = server.mock(|when, then| {
424 when.method(Method::GET).path("/test");
425 then.status(200).json_body(json!({"success": true}));
426 });
427
428 let client = test_client();
429 let url = format!("{}/test", server.base_url());
430 let resp = client.get(&url).send().await.unwrap();
431
432 assert_eq!(resp.status(), hyper::StatusCode::OK);
433 }
434
435 #[tokio::test]
436 async fn test_http_client_post() {
437 let server = MockServer::start();
438 let _m = server.mock(|when, then| {
439 when.method(Method::POST).path("/action");
440 then.status(200).json_body(json!({"ok": true}));
441 });
442
443 let client = test_client();
444 let url = format!("{}/action", server.base_url());
445 let resp = client.post(&url).send().await.unwrap();
446
447 assert_eq!(resp.status(), hyper::StatusCode::OK);
448 }
449
450 #[tokio::test]
451 async fn test_http_client_post_form() {
452 let server = MockServer::start();
453 let _m = server.mock(|when, then| {
454 when.method(Method::POST)
455 .path("/submit")
456 .header("content-type", "application/x-www-form-urlencoded")
457 .body("key1=value1&key2=value2");
458 then.status(200).json_body(json!({"received": true}));
459 });
460
461 let client = test_client();
462 let url = format!("{}/submit", server.base_url());
463
464 let resp = client
465 .post(&url)
466 .form(&[("key1", "value1"), ("key2", "value2")])
467 .unwrap()
468 .send()
469 .await
470 .unwrap();
471 assert_eq!(resp.status(), hyper::StatusCode::OK);
472 }
473
474 #[tokio::test]
475 async fn test_json_body_parsing() {
476 #[derive(serde::Deserialize)]
477 struct TestResponse {
478 name: String,
479 value: i32,
480 }
481
482 let server = MockServer::start();
483 let _m = server.mock(|when, then| {
484 when.method(Method::GET).path("/json");
485 then.status(200)
486 .json_body(json!({"name": "test", "value": 42}));
487 });
488
489 let client = test_client();
490 let url = format!("{}/json", server.base_url());
491
492 let data: TestResponse = client.get(&url).send().await.unwrap().json().await.unwrap();
493 assert_eq!(data.name, "test");
494 assert_eq!(data.value, 42);
495 }
496
497 #[tokio::test]
498 async fn test_body_size_limit() {
499 let server = MockServer::start();
500 let large_body = "x".repeat(1024 * 1024); let _m = server.mock(|when, then| {
502 when.method(Method::GET).path("/large");
503 then.status(200).body(&large_body);
504 });
505
506 let client = HttpClientBuilder::new()
507 .retry(None)
508 .max_body_size(1024) .build()
510 .unwrap();
511
512 let url = format!("{}/large", server.base_url());
513 let result = client.get(&url).send().await.unwrap().bytes().await;
514
515 assert!(matches!(result, Err(HttpError::BodyTooLarge { .. })));
516 }
517
518 #[tokio::test]
519 async fn test_custom_user_agent() {
520 let server = MockServer::start();
521 let _m = server.mock(|when, then| {
522 when.method(Method::GET)
523 .path("/test")
524 .header("user-agent", "custom/1.0");
525 then.status(200);
526 });
527
528 let client = HttpClientBuilder::new()
529 .retry(None)
530 .user_agent("custom/1.0")
531 .build()
532 .unwrap();
533
534 let url = format!("{}/test", server.base_url());
535 let resp = client.get(&url).send().await.unwrap();
536 assert_eq!(resp.status(), hyper::StatusCode::OK);
537 }
538
539 #[tokio::test]
540 async fn test_non_2xx_returns_http_status_error() {
541 let server = MockServer::start();
542 let _m = server.mock(|when, then| {
543 when.method(Method::GET).path("/error");
544 then.status(404)
545 .header("content-type", "application/json")
546 .body(r#"{"error": "not found"}"#);
547 });
548
549 let client = test_client();
550 let url = format!("{}/error", server.base_url());
551
552 let result: Result<serde_json::Value, _> =
553 client.get(&url).send().await.unwrap().json().await;
554 match result {
555 Err(HttpError::HttpStatus {
556 status,
557 body_preview,
558 content_type,
559 ..
560 }) => {
561 assert_eq!(status, hyper::StatusCode::NOT_FOUND);
562 assert!(body_preview.contains("not found"));
563 assert_eq!(content_type, Some("application/json".to_owned()));
564 }
565 other => panic!("Expected HttpStatus error, got: {other:?}"),
566 }
567 }
568
569 #[tokio::test]
570 async fn test_checked_body_success() {
571 let server = MockServer::start();
572 let _m = server.mock(|when, then| {
573 when.method(Method::GET).path("/data");
574 then.status(200).body("hello world");
575 });
576
577 let client = test_client();
578 let url = format!("{}/data", server.base_url());
579
580 let body = client
581 .get(&url)
582 .send()
583 .await
584 .unwrap()
585 .checked_bytes()
586 .await
587 .unwrap();
588 assert_eq!(&body[..], b"hello world");
589 }
590
591 #[tokio::test]
592 async fn test_client_is_clone() {
593 let client = test_client();
594 let client2 = client.clone();
595
596 let server = MockServer::start();
598 let _m = server.mock(|when, then| {
599 when.method(Method::GET).path("/test");
600 then.status(200);
601 });
602
603 let url = format!("{}/test", server.base_url());
604 let resp1 = client.get(&url).send().await.unwrap();
605 let resp2 = client2.get(&url).send().await.unwrap();
606
607 assert_eq!(resp1.status(), hyper::StatusCode::OK);
608 assert_eq!(resp2.status(), hyper::StatusCode::OK);
609 }
610
611 #[test]
615 fn test_http_client_is_send_sync() {
616 fn assert_send_sync<T: Send + Sync>() {}
617 assert_send_sync::<HttpClient>();
618 }
619
620 #[tokio::test]
622 async fn test_concurrent_requests_50() {
623 let server = MockServer::start();
624 let _m = server.mock(|when, then| {
625 when.method(Method::GET).path("/concurrent");
626 then.status(200).body("ok");
627 });
628
629 let client = test_client();
630 let url = format!("{}/concurrent", server.base_url());
631
632 let handles: Vec<_> = (0..50)
634 .map(|_| {
635 let client = client.clone();
636 let url = url.clone();
637 tokio::spawn(async move { client.get(&url).send().await })
638 })
639 .collect();
640
641 for handle in handles {
643 let resp = handle.await.unwrap().unwrap();
644 assert_eq!(resp.status(), hyper::StatusCode::OK);
645 }
646 }
647
648 #[tokio::test]
651 async fn test_request_id_forwarded_when_in_scope() {
652 let server = MockServer::start();
653 let _m = server.mock(|when, then| {
654 when.method(Method::GET)
655 .path("/test")
656 .header("x-request-id", "req-42");
657 then.status(200);
658 });
659
660 let client = test_client();
661 let url = format!("{}/test", server.base_url());
662
663 let resp = toolkit_trace_context::with_request_id(
664 Some("req-42".to_owned()),
665 client.get(&url).send(),
666 )
667 .await
668 .unwrap();
669 assert_eq!(resp.status(), hyper::StatusCode::OK);
670 }
671
672 #[tokio::test]
675 async fn test_request_id_explicit_header_wins_over_scope() {
676 let server = MockServer::start();
677 let _m = server.mock(|when, then| {
678 when.method(Method::GET)
679 .path("/test")
680 .header("x-request-id", "explicit-id");
681 then.status(200);
682 });
683
684 let client = test_client();
685 let url = format!("{}/test", server.base_url());
686
687 let resp = toolkit_trace_context::with_request_id(
688 Some("scoped-id".to_owned()),
689 client
690 .get(&url)
691 .header("x-request-id", "explicit-id")
692 .send(),
693 )
694 .await
695 .unwrap();
696 assert_eq!(resp.status(), hyper::StatusCode::OK);
697 }
698
699 #[tokio::test]
702 async fn test_request_id_absent_outside_scope() {
703 let server = MockServer::start();
704 let mock = server.mock(|when, then| {
705 when.method(Method::GET)
706 .path("/test")
707 .header_missing("x-request-id");
708 then.status(200);
709 });
710
711 let client = test_client();
712 let url = format!("{}/test", server.base_url());
713 let resp = client.get(&url).send().await.unwrap();
714 assert_eq!(resp.status(), hyper::StatusCode::OK);
715 mock.assert_async().await;
716 }
717
718 #[tokio::test]
726 async fn test_small_buffer_capacity_no_deadlock() {
727 use crate::config::HttpClientConfig;
728
729 let server = MockServer::start();
730 let _m = server.mock(|when, then| {
731 when.method(Method::GET).path("/test");
732 then.status(200).body("ok");
733 });
734
735 let config = HttpClientConfig {
737 transport: crate::config::TransportSecurity::AllowInsecureHttp,
738 retry: None,
739 rate_limit: None,
740 buffer_capacity: 2,
741 ..Default::default()
742 };
743
744 let client = HttpClientBuilder::with_config(config).build().unwrap();
745 let url = format!("{}/test", server.base_url());
746
747 let handles: Vec<_> = (0..10)
749 .map(|_| {
750 let client = client.clone();
751 let url = url.clone();
752 tokio::spawn(async move { client.get(&url).send().await })
753 })
754 .collect();
755
756 let timeout_result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
758 let mut results = Vec::new();
759 for handle in handles {
760 results.push(handle.await);
761 }
762 results
763 })
764 .await;
765
766 let results = timeout_result.expect("requests should complete within timeout");
767
768 let mut success_count = 0;
769 let mut overloaded_count = 0;
770 for result in results {
771 match result.unwrap() {
772 Ok(resp) => {
773 assert_eq!(resp.status(), hyper::StatusCode::OK);
774 success_count += 1;
775 }
776 Err(HttpError::Overloaded) => {
777 overloaded_count += 1;
778 }
779 Err(e) => panic!("unexpected error: {e:?}"),
780 }
781 }
782
783 assert!(success_count > 0, "at least one request should succeed");
785 assert_eq!(success_count + overloaded_count, 10);
787 }
788
789 #[tokio::test]
794 async fn test_buffer_overflow_returns_overloaded() {
795 use crate::config::HttpClientConfig;
796
797 let server = MockServer::start();
798
799 let _m = server.mock(|when, then| {
800 when.method(Method::GET).path("/slow");
801 then.status(200).body("ok");
802 });
803
804 let config = HttpClientConfig {
806 transport: crate::config::TransportSecurity::AllowInsecureHttp,
807 retry: None,
808 rate_limit: None,
809 buffer_capacity: 1,
810 ..Default::default()
811 };
812
813 let client = HttpClientBuilder::with_config(config).build().unwrap();
814 let url = format!("{}/slow", server.base_url());
815
816 let client1 = client.clone();
818 let url1 = url.clone();
819 let handle1 = tokio::spawn(async move { client1.get(&url1).send().await });
820
821 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
823
824 let result2 = tokio::time::timeout(
826 std::time::Duration::from_millis(50),
827 client.get(&url).send(),
828 )
829 .await;
830
831 let inner_result = result2.expect("request should not timeout waiting for buffer");
833 match inner_result {
834 Err(HttpError::Overloaded) | Ok(_) => {}
836 Err(e) => panic!("unexpected error: {e:?}"),
837 }
838
839 _ = handle1.await;
841 }
842
843 #[tokio::test]
845 async fn test_large_body_no_deadlock() {
846 let server = MockServer::start();
847 let large_body = "x".repeat(100 * 1024); let _m = server.mock(|when, then| {
849 when.method(Method::GET).path("/large");
850 then.status(200).body(&large_body);
851 });
852
853 let client = HttpClientBuilder::new()
854 .retry(None)
855 .max_body_size(1024 * 1024) .build()
857 .unwrap();
858
859 let url = format!("{}/large", server.base_url());
860
861 let handles: Vec<_> = (0..5)
863 .map(|_| {
864 let client = client.clone();
865 let url = url.clone();
866 tokio::spawn(async move { client.get(&url).send().await?.checked_bytes().await })
867 })
868 .collect();
869
870 let timeout_result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
872 let mut results = Vec::new();
873 for handle in handles {
874 results.push(handle.await);
875 }
876 results
877 })
878 .await;
879
880 let results = timeout_result.expect("body reads should complete within timeout");
881 for result in results {
882 let body = result.unwrap().unwrap();
883 assert_eq!(body.len(), 100 * 1024);
884 }
885 }
886
887 #[tokio::test]
893 async fn test_token_endpoint_post_not_retried() {
894 use crate::config::HttpClientConfig;
895
896 let server = MockServer::start();
897
898 let mock = server.mock(|when, then| {
900 when.method(Method::POST).path("/token");
901 then.status(500).body("server error");
902 });
903
904 let mut config = HttpClientConfig::token_endpoint();
906 config.transport = crate::config::TransportSecurity::AllowInsecureHttp; let client = HttpClientBuilder::with_config(config).build().unwrap();
909 let url = format!("{}/token", server.base_url());
910
911 let result = client
913 .post(&url)
914 .form(&[("grant_type", "client_credentials"), ("client_id", "test")])
915 .unwrap()
916 .send()
917 .await;
918
919 assert!(result.is_ok()); let response = result.unwrap();
922 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
923
924 assert_eq!(
927 mock.calls(),
928 1,
929 "POST should not be retried; expected 1 call, got {}",
930 mock.calls()
931 );
932 }
933
934 #[tokio::test]
940 async fn test_http_client_put() {
941 let server = MockServer::start();
942 let _m = server.mock(|when, then| {
943 when.method(Method::PUT).path("/resource");
944 then.status(200).json_body(json!({"updated": true}));
945 });
946
947 let client = test_client();
948 let url = format!("{}/resource", server.base_url());
949 let resp = client.put(&url).send().await.unwrap();
950
951 assert_eq!(resp.status(), hyper::StatusCode::OK);
952 }
953
954 #[tokio::test]
955 async fn test_http_client_put_form() {
956 let server = MockServer::start();
957 let _m = server.mock(|when, then| {
958 when.method(Method::PUT)
959 .path("/resource")
960 .header("content-type", "application/x-www-form-urlencoded")
961 .body("name=updated&value=123");
962 then.status(200).json_body(json!({"updated": true}));
963 });
964
965 let client = test_client();
966 let url = format!("{}/resource", server.base_url());
967
968 let resp = client
969 .put(&url)
970 .form(&[("name", "updated"), ("value", "123")])
971 .unwrap()
972 .send()
973 .await
974 .unwrap();
975 assert_eq!(resp.status(), hyper::StatusCode::OK);
976 }
977
978 #[tokio::test]
979 async fn test_http_client_patch() {
980 let server = MockServer::start();
981 let _m = server.mock(|when, then| {
982 when.method(Method::PATCH).path("/resource/1");
983 then.status(200).json_body(json!({"patched": true}));
984 });
985
986 let client = test_client();
987 let url = format!("{}/resource/1", server.base_url());
988 let resp = client.patch(&url).send().await.unwrap();
989
990 assert_eq!(resp.status(), hyper::StatusCode::OK);
991 }
992
993 #[tokio::test]
994 async fn test_http_client_patch_form() {
995 let server = MockServer::start();
996 let _m = server.mock(|when, then| {
997 when.method(Method::PATCH)
998 .path("/resource/1")
999 .header("content-type", "application/x-www-form-urlencoded")
1000 .body("field=patched");
1001 then.status(200).json_body(json!({"patched": true}));
1002 });
1003
1004 let client = test_client();
1005 let url = format!("{}/resource/1", server.base_url());
1006
1007 let resp = client
1008 .patch(&url)
1009 .form(&[("field", "patched")])
1010 .unwrap()
1011 .send()
1012 .await
1013 .unwrap();
1014 assert_eq!(resp.status(), hyper::StatusCode::OK);
1015 }
1016
1017 #[tokio::test]
1018 async fn test_http_client_delete() {
1019 let server = MockServer::start();
1020 let _m = server.mock(|when, then| {
1021 when.method(Method::DELETE).path("/resource/42");
1022 then.status(204);
1023 });
1024
1025 let client = test_client();
1026 let url = format!("{}/resource/42", server.base_url());
1027 let resp = client.delete(&url).send().await.unwrap();
1028
1029 assert_eq!(resp.status(), hyper::StatusCode::NO_CONTENT);
1030 }
1031
1032 #[tokio::test]
1033 async fn test_http_client_delete_returns_200() {
1034 let server = MockServer::start();
1035 let _m = server.mock(|when, then| {
1036 when.method(Method::DELETE).path("/resource/99");
1037 then.status(200).json_body(json!({"deleted": true}));
1038 });
1039
1040 let client = test_client();
1041 let url = format!("{}/resource/99", server.base_url());
1042 let resp = client.delete(&url).send().await.unwrap();
1043
1044 assert_eq!(resp.status(), hyper::StatusCode::OK);
1045 }
1046
1047 #[tokio::test]
1048 async fn test_http_client_head_returns_headers_no_body() {
1049 let server = MockServer::start();
1050 let _m = server.mock(|when, then| {
1051 when.method(Method::HEAD).path("/probe");
1052 then.status(200)
1053 .header("content-length", "12345")
1054 .header("etag", "\"abc\"");
1055 });
1056
1057 let client = test_client();
1058 let url = format!("{}/probe", server.base_url());
1059 let resp = client.head(&url).send().await.unwrap();
1060
1061 assert_eq!(resp.status(), hyper::StatusCode::OK);
1062 assert_eq!(
1063 resp.headers()
1064 .get("content-length")
1065 .and_then(|v| v.to_str().ok()),
1066 Some("12345"),
1067 );
1068 let body = resp.bytes().await.unwrap();
1069 assert!(body.is_empty(), "HEAD response body must be empty");
1070 }
1071
1072 #[tokio::test]
1073 async fn test_http_client_options_returns_allow_header() {
1074 let server = MockServer::start();
1075 let _m = server.mock(|when, then| {
1076 when.method(Method::OPTIONS).path("/resource");
1077 then.status(204).header("allow", "GET, POST, PUT, DELETE");
1078 });
1079
1080 let client = test_client();
1081 let url = format!("{}/resource", server.base_url());
1082 let resp = client.options(&url).send().await.unwrap();
1083
1084 assert_eq!(resp.status(), hyper::StatusCode::NO_CONTENT);
1085 assert_eq!(
1086 resp.headers().get("allow").and_then(|v| v.to_str().ok()),
1087 Some("GET, POST, PUT, DELETE"),
1088 );
1089 }
1090
1091 #[tokio::test]
1092 async fn test_put_form_with_custom_headers() {
1093 let server = MockServer::start();
1094 let _m = server.mock(|when, then| {
1095 when.method(Method::PUT)
1096 .path("/api/data")
1097 .header("content-type", "application/x-www-form-urlencoded")
1098 .header("x-custom-header", "custom-value")
1099 .body("key=value");
1100 then.status(200);
1101 });
1102
1103 let client = test_client();
1104 let url = format!("{}/api/data", server.base_url());
1105
1106 let resp = client
1107 .put(&url)
1108 .header("x-custom-header", "custom-value")
1109 .form(&[("key", "value")])
1110 .unwrap()
1111 .send()
1112 .await
1113 .unwrap();
1114 assert_eq!(resp.status(), hyper::StatusCode::OK);
1115 }
1116
1117 #[tokio::test]
1118 async fn test_patch_form_with_custom_headers() {
1119 let server = MockServer::start();
1120 let _m = server.mock(|when, then| {
1121 when.method(Method::PATCH)
1122 .path("/api/data")
1123 .header("content-type", "application/x-www-form-urlencoded")
1124 .header("authorization", "Bearer token123")
1125 .body("status=active");
1126 then.status(200);
1127 });
1128
1129 let client = test_client();
1130 let url = format!("{}/api/data", server.base_url());
1131
1132 let resp = client
1133 .patch(&url)
1134 .header("authorization", "Bearer token123")
1135 .form(&[("status", "active")])
1136 .unwrap()
1137 .send()
1138 .await
1139 .unwrap();
1140 assert_eq!(resp.status(), hyper::StatusCode::OK);
1141 }
1142
1143 #[tokio::test]
1144 async fn test_request_builder_json_body() {
1145 #[derive(serde::Serialize)]
1146 struct CreateUser {
1147 name: String,
1148 email: String,
1149 }
1150
1151 let server = MockServer::start();
1152 let _m = server.mock(|when, then| {
1153 when.method(Method::POST)
1154 .path("/users")
1155 .header("content-type", "application/json")
1156 .json_body(json!({"name": "Alice", "email": "alice@example.com"}));
1157 then.status(201).json_body(json!({"id": 1}));
1158 });
1159
1160 let client = test_client();
1161 let url = format!("{}/users", server.base_url());
1162
1163 let resp = client
1164 .post(&url)
1165 .json(&CreateUser {
1166 name: "Alice".into(),
1167 email: "alice@example.com".into(),
1168 })
1169 .unwrap()
1170 .send()
1171 .await
1172 .unwrap();
1173 assert_eq!(resp.status(), hyper::StatusCode::CREATED);
1174 }
1175
1176 #[tokio::test]
1177 async fn test_request_builder_body_bytes() {
1178 let server = MockServer::start();
1179 let _m = server.mock(|when, then| {
1180 when.method(Method::POST)
1181 .path("/upload")
1182 .body("raw binary data");
1183 then.status(200);
1184 });
1185
1186 let client = test_client();
1187 let url = format!("{}/upload", server.base_url());
1188
1189 let resp = client
1190 .post(&url)
1191 .body_bytes(bytes::Bytes::from("raw binary data"))
1192 .send()
1193 .await
1194 .unwrap();
1195 assert_eq!(resp.status(), hyper::StatusCode::OK);
1196 }
1197
1198 #[tokio::test]
1204 async fn test_content_type_not_duplicated_with_json() {
1205 #[derive(serde::Serialize)]
1206 struct TestData {
1207 value: i32,
1208 }
1209
1210 let server = MockServer::start();
1211 let mock = server.mock(|when, then| {
1212 when.method(Method::POST)
1213 .path("/custom-content-type")
1214 .header("content-type", "application/vnd.custom+json");
1216 then.status(200);
1217 });
1218
1219 let client = test_client();
1220 let url = format!("{}/custom-content-type", server.base_url());
1221
1222 let resp = client
1223 .post(&url)
1224 .header("content-type", "application/vnd.custom+json") .json(&TestData { value: 42 })
1226 .unwrap()
1227 .send()
1228 .await
1229 .unwrap();
1230
1231 assert_eq!(resp.status(), hyper::StatusCode::OK);
1232 assert_eq!(
1233 mock.calls(),
1234 1,
1235 "Request with custom Content-Type should match"
1236 );
1237 }
1238
1239 #[tokio::test]
1241 async fn test_content_type_not_duplicated_with_form() {
1242 let server = MockServer::start();
1243 let mock = server.mock(|when, then| {
1244 when.method(Method::POST)
1245 .path("/custom-form-type")
1246 .header("content-type", "application/x-custom-form");
1248 then.status(200);
1249 });
1250
1251 let client = test_client();
1252 let url = format!("{}/custom-form-type", server.base_url());
1253
1254 let resp = client
1255 .post(&url)
1256 .header("content-type", "application/x-custom-form") .form(&[("key", "value")])
1258 .unwrap()
1259 .send()
1260 .await
1261 .unwrap();
1262
1263 assert_eq!(resp.status(), hyper::StatusCode::OK);
1264 assert_eq!(
1265 mock.calls(),
1266 1,
1267 "Request with custom Content-Type should match"
1268 );
1269 }
1270
1271 #[tokio::test]
1272 async fn test_request_builder_body_string() {
1273 let server = MockServer::start();
1274 let _m = server.mock(|when, then| {
1275 when.method(Method::POST)
1276 .path("/text")
1277 .body("Hello, World!");
1278 then.status(200);
1279 });
1280
1281 let client = test_client();
1282 let url = format!("{}/text", server.base_url());
1283
1284 let resp = client
1285 .post(&url)
1286 .body_string("Hello, World!".into())
1287 .send()
1288 .await
1289 .unwrap();
1290 assert_eq!(resp.status(), hyper::StatusCode::OK);
1291 }
1292
1293 #[tokio::test]
1294 async fn test_response_text_method() {
1295 let server = MockServer::start();
1296 let _m = server.mock(|when, then| {
1297 when.method(Method::GET).path("/text");
1298 then.status(200).body("Hello, World!");
1299 });
1300
1301 let client = test_client();
1302 let url = format!("{}/text", server.base_url());
1303
1304 let text = client.get(&url).send().await.unwrap().text().await.unwrap();
1305 assert_eq!(text, "Hello, World!");
1306 }
1307
1308 #[tokio::test]
1309 async fn test_request_builder_multiple_headers() {
1310 let server = MockServer::start();
1311 let _m = server.mock(|when, then| {
1312 when.method(Method::GET)
1313 .path("/headers")
1314 .header("x-first", "one")
1315 .header("x-second", "two");
1316 then.status(200);
1317 });
1318
1319 let client = test_client();
1320 let url = format!("{}/headers", server.base_url());
1321
1322 let resp = client
1323 .get(&url)
1324 .header("x-first", "one")
1325 .header("x-second", "two")
1326 .send()
1327 .await
1328 .unwrap();
1329 assert_eq!(resp.status(), hyper::StatusCode::OK);
1330 }
1331
1332 #[tokio::test]
1333 async fn test_request_builder_headers_vec() {
1334 let server = MockServer::start();
1335 let _m = server.mock(|when, then| {
1336 when.method(Method::GET)
1337 .path("/headers")
1338 .header("x-first", "one")
1339 .header("x-second", "two");
1340 then.status(200);
1341 });
1342
1343 let client = test_client();
1344 let url = format!("{}/headers", server.base_url());
1345
1346 let resp = client
1347 .get(&url)
1348 .headers(vec![
1349 ("x-first".to_owned(), "one".to_owned()),
1350 ("x-second".to_owned(), "two".to_owned()),
1351 ])
1352 .send()
1353 .await
1354 .unwrap();
1355 assert_eq!(resp.status(), hyper::StatusCode::OK);
1356 }
1357
1358 #[tokio::test]
1361 async fn test_error_response_with_large_body_returns_http_status() {
1362 use crate::security::ERROR_BODY_PREVIEW_LIMIT;
1363
1364 let server = MockServer::start();
1365
1366 let large_body = "x".repeat(ERROR_BODY_PREVIEW_LIMIT + 1000);
1368
1369 let _m = server.mock(|when, then| {
1370 when.method(Method::GET).path("/error-with-large-body");
1371 then.status(500).body(&large_body);
1372 });
1373
1374 let client = test_client();
1375 let url = format!("{}/error-with-large-body", server.base_url());
1376
1377 let result = client.get(&url).send().await.unwrap().checked_bytes().await;
1378
1379 match result {
1381 Err(HttpError::HttpStatus {
1382 status,
1383 body_preview,
1384 ..
1385 }) => {
1386 assert_eq!(status, hyper::StatusCode::INTERNAL_SERVER_ERROR);
1387 assert_eq!(body_preview, "<body too large for preview>");
1389 }
1390 Err(HttpError::BodyTooLarge { .. }) => {
1391 panic!("Should return HttpStatus, not BodyTooLarge for non-2xx responses");
1392 }
1393 Err(other) => panic!("Unexpected error: {other:?}"),
1394 Ok(_) => panic!("Should have returned an error for 500 status"),
1395 }
1396 }
1397
1398 fn gzip_compress(data: &[u8]) -> Vec<u8> {
1404 use flate2::Compression;
1405 use flate2::write::GzEncoder;
1406 use std::io::Write;
1407
1408 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1409 encoder.write_all(data).unwrap();
1410 encoder.finish().unwrap()
1411 }
1412
1413 #[tokio::test]
1418 async fn test_gzip_decompression_basic() {
1419 let server = MockServer::start();
1420
1421 let original_body = b"Hello, this is a test body that will be gzip compressed!";
1422 let compressed_body = gzip_compress(original_body);
1423
1424 let _m = server.mock(|when, then| {
1425 when.method(Method::GET).path("/gzip");
1426 then.status(200)
1427 .header("content-encoding", "gzip")
1428 .body(compressed_body);
1429 });
1430
1431 let client = test_client();
1432 let url = format!("{}/gzip", server.base_url());
1433
1434 let body = client
1435 .get(&url)
1436 .send()
1437 .await
1438 .unwrap()
1439 .bytes()
1440 .await
1441 .unwrap();
1442
1443 assert_eq!(
1444 body.as_ref(),
1445 original_body,
1446 "Decompressed body should match original"
1447 );
1448 }
1449
1450 #[tokio::test]
1455 async fn test_gzip_decompression_json() {
1456 #[derive(serde::Deserialize, PartialEq, Debug)]
1457 struct TestData {
1458 name: String,
1459 value: i32,
1460 nested: NestedData,
1461 }
1462
1463 #[derive(serde::Deserialize, PartialEq, Debug)]
1464 struct NestedData {
1465 items: Vec<String>,
1466 }
1467
1468 let server = MockServer::start();
1469
1470 let json_body = r#"{"name":"test","value":42,"nested":{"items":["a","b","c"]}}"#;
1471 let compressed_body = gzip_compress(json_body.as_bytes());
1472
1473 let _m = server.mock(|when, then| {
1474 when.method(Method::GET).path("/gzip-json");
1475 then.status(200)
1476 .header("content-type", "application/json")
1477 .header("content-encoding", "gzip")
1478 .body(compressed_body);
1479 });
1480
1481 let client = test_client();
1482 let url = format!("{}/gzip-json", server.base_url());
1483
1484 let data: TestData = client.get(&url).send().await.unwrap().json().await.unwrap();
1485
1486 assert_eq!(data.name, "test");
1487 assert_eq!(data.value, 42);
1488 assert_eq!(data.nested.items, vec!["a", "b", "c"]);
1489 }
1490
1491 #[tokio::test]
1499 async fn test_gzip_decompression_body_size_limit() {
1500 let server = MockServer::start();
1501
1502 let large_decompressed = vec![b'x'; 100 * 1024]; let compressed_body = gzip_compress(&large_decompressed);
1506
1507 assert!(
1509 compressed_body.len() < 2000,
1510 "Compressed body should be small (got {} bytes)",
1511 compressed_body.len()
1512 );
1513
1514 let _m = server.mock(|when, then| {
1515 when.method(Method::GET).path("/gzip-bomb");
1516 then.status(200)
1517 .header("content-encoding", "gzip")
1518 .body(compressed_body);
1519 });
1520
1521 let client = HttpClientBuilder::new()
1523 .retry(None)
1524 .max_body_size(10 * 1024) .build()
1526 .unwrap();
1527
1528 let url = format!("{}/gzip-bomb", server.base_url());
1529 let result = client.get(&url).send().await.unwrap().bytes().await;
1530
1531 match result {
1533 Err(HttpError::BodyTooLarge { limit, actual }) => {
1534 assert_eq!(limit, 10 * 1024, "Limit should be 10KB");
1535 assert!(
1536 actual > limit,
1537 "Actual size ({actual}) should exceed limit ({limit})"
1538 );
1539 }
1540 Err(other) => panic!("Expected BodyTooLarge error, got: {other:?}"),
1541 Ok(body) => panic!(
1542 "Expected BodyTooLarge error, but got {} bytes of body",
1543 body.len()
1544 ),
1545 }
1546 }
1547
1548 #[tokio::test]
1553 async fn test_accept_encoding_header_sent() {
1554 let server = MockServer::start();
1555
1556 let mock = server.mock(|when, then| {
1558 when.method(Method::GET)
1559 .path("/check-accept-encoding")
1560 .header_exists("accept-encoding");
1561 then.status(200).body("ok");
1562 });
1563
1564 let client = test_client();
1565 let url = format!("{}/check-accept-encoding", server.base_url());
1566
1567 let resp = client.get(&url).send().await.unwrap();
1568 assert_eq!(resp.status(), hyper::StatusCode::OK);
1569
1570 assert_eq!(
1572 mock.calls(),
1573 1,
1574 "Request should have included Accept-Encoding header"
1575 );
1576 }
1577
1578 #[tokio::test]
1582 async fn test_no_compression_passthrough() {
1583 let server = MockServer::start();
1584
1585 let plain_body = b"This is plain text, not compressed";
1586
1587 let _m = server.mock(|when, then| {
1588 when.method(Method::GET).path("/plain");
1589 then.status(200)
1590 .header("content-type", "text/plain")
1591 .body(plain_body.as_slice());
1592 });
1593
1594 let client = test_client();
1595 let url = format!("{}/plain", server.base_url());
1596
1597 let body = client
1598 .get(&url)
1599 .send()
1600 .await
1601 .unwrap()
1602 .bytes()
1603 .await
1604 .unwrap();
1605
1606 assert_eq!(
1607 body.as_ref(),
1608 plain_body,
1609 "Plain body should pass through unchanged"
1610 );
1611 }
1612
1613 #[tokio::test]
1615 async fn test_gzip_decompression_checked_bytes() {
1616 let server = MockServer::start();
1617
1618 let original_body = b"Checked bytes test with gzip";
1619 let compressed_body = gzip_compress(original_body);
1620
1621 let _m = server.mock(|when, then| {
1622 when.method(Method::GET).path("/gzip-checked");
1623 then.status(200)
1624 .header("content-encoding", "gzip")
1625 .body(compressed_body);
1626 });
1627
1628 let client = test_client();
1629 let url = format!("{}/gzip-checked", server.base_url());
1630
1631 let body = client
1632 .get(&url)
1633 .send()
1634 .await
1635 .unwrap()
1636 .checked_bytes()
1637 .await
1638 .unwrap();
1639
1640 assert_eq!(
1641 body.as_ref(),
1642 original_body,
1643 "checked_bytes should return decompressed content"
1644 );
1645 }
1646
1647 #[tokio::test]
1649 async fn test_gzip_decompression_text() {
1650 let server = MockServer::start();
1651
1652 let original_text = "Hello, World! \u{1F600}"; let compressed_body = gzip_compress(original_text.as_bytes());
1654
1655 let _m = server.mock(|when, then| {
1656 when.method(Method::GET).path("/gzip-text");
1657 then.status(200)
1658 .header("content-type", "text/plain; charset=utf-8")
1659 .header("content-encoding", "gzip")
1660 .body(compressed_body);
1661 });
1662
1663 let client = test_client();
1664 let url = format!("{}/gzip-text", server.base_url());
1665
1666 let text = client.get(&url).send().await.unwrap().text().await.unwrap();
1667
1668 assert_eq!(
1669 text, original_text,
1670 "text() should return decompressed UTF-8 content"
1671 );
1672 }
1673
1674 #[test]
1680 fn test_map_buffer_error_passes_through_http_error() {
1681 let http_err = HttpError::Timeout(std::time::Duration::from_secs(10));
1682 let boxed: tower::BoxError = Box::new(http_err);
1683 let result = map_buffer_error(boxed);
1684
1685 assert!(
1686 matches!(result, HttpError::Timeout(_)),
1687 "Should pass through HttpError::Timeout, got: {result:?}"
1688 );
1689 }
1690
1691 #[test]
1696 fn test_map_buffer_error_returns_service_closed_for_unknown_error() {
1697 let other_err: tower::BoxError = Box::new(std::io::Error::new(
1699 std::io::ErrorKind::BrokenPipe,
1700 "buffer worker died",
1701 ));
1702 let result = map_buffer_error(other_err);
1703
1704 assert!(
1705 matches!(result, HttpError::ServiceClosed),
1706 "Should return ServiceClosed for non-HttpError, got: {result:?}"
1707 );
1708 }
1709
1710 #[tokio::test]
1727 async fn test_status_retry_get_500_retried() {
1728 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1729
1730 let server = MockServer::start();
1731 let mock = server.mock(|when, then| {
1732 when.method(Method::GET).path("/retry-500");
1733 then.status(500).body("server error");
1734 });
1735
1736 let config = HttpClientConfig {
1737 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1738 retry: Some(RetryConfig {
1739 max_retries: 2, backoff: ExponentialBackoff::fast(),
1741 ..RetryConfig::default() }),
1743 rate_limit: None,
1744 ..Default::default()
1745 };
1746
1747 let client = HttpClientBuilder::with_config(config).build().unwrap();
1748 let url = format!("{}/retry-500", server.base_url());
1749
1750 let result = client.get(&url).send().await;
1751
1752 assert_eq!(
1754 mock.calls(),
1755 3,
1756 "GET should retry on 500; expected 3 calls (1 + 2 retries), got {}",
1757 mock.calls()
1758 );
1759
1760 let response = result.expect("send() should return Ok(Response) after retries exhaust");
1762 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
1763
1764 let err = response.error_for_status().unwrap_err();
1766 assert!(
1767 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
1768 );
1769 }
1770
1771 #[tokio::test]
1780 async fn test_status_retry_post_500_not_retried() {
1781 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1782
1783 let server = MockServer::start();
1784 let mock = server.mock(|when, then| {
1785 when.method(Method::POST).path("/post-500");
1786 then.status(500).body("server error");
1787 });
1788
1789 let config = HttpClientConfig {
1790 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1791 retry: Some(RetryConfig {
1792 max_retries: 3,
1793 backoff: ExponentialBackoff::fast(),
1794 ..RetryConfig::default() }),
1796 rate_limit: None,
1797 ..Default::default()
1798 };
1799
1800 let client = HttpClientBuilder::with_config(config).build().unwrap();
1801 let url = format!("{}/post-500", server.base_url());
1802
1803 let result = client.post(&url).send().await;
1804
1805 assert_eq!(
1807 mock.calls(),
1808 1,
1809 "POST should not be retried on 500; expected 1 call, got {}",
1810 mock.calls()
1811 );
1812
1813 let response = result.expect("POST + 500 should return Ok(Response), not Err");
1816 assert_eq!(
1817 response.status(),
1818 hyper::StatusCode::INTERNAL_SERVER_ERROR,
1819 "Response should have 500 status"
1820 );
1821
1822 }
1824
1825 #[tokio::test]
1831 async fn test_status_retry_post_429_retried() {
1832 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1833
1834 let server = MockServer::start();
1835 let mock = server.mock(|when, then| {
1836 when.method(Method::POST).path("/post-429");
1837 then.status(429).body("rate limited");
1838 });
1839
1840 let config = HttpClientConfig {
1841 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1842 retry: Some(RetryConfig {
1843 max_retries: 2, backoff: ExponentialBackoff::fast(),
1845 ..RetryConfig::default() }),
1847 rate_limit: None,
1848 ..Default::default()
1849 };
1850
1851 let client = HttpClientBuilder::with_config(config).build().unwrap();
1852 let url = format!("{}/post-429", server.base_url());
1853
1854 let result = client.post(&url).send().await;
1855
1856 assert_eq!(
1858 mock.calls(),
1859 3,
1860 "POST should retry on 429; expected 3 calls (1 + 2 retries), got {}",
1861 mock.calls()
1862 );
1863
1864 let response = result.expect("send() should return Ok(Response) after retries exhaust");
1866 assert_eq!(response.status(), hyper::StatusCode::TOO_MANY_REQUESTS);
1867
1868 let err = response.error_for_status().unwrap_err();
1870 assert!(
1871 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::TOO_MANY_REQUESTS)
1872 );
1873 }
1874
1875 #[tokio::test]
1880 async fn test_status_retry_extracts_retry_after_header() {
1881 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1882
1883 let server = MockServer::start();
1884 let _mock = server.mock(|when, then| {
1885 when.method(Method::GET).path("/retry-after");
1886 then.status(429)
1887 .header("Retry-After", "60")
1888 .header("Content-Type", "application/json")
1889 .body(r#"{"error": "rate limited"}"#);
1890 });
1891
1892 let config = HttpClientConfig {
1893 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1894 retry: Some(RetryConfig {
1895 max_retries: 0, backoff: ExponentialBackoff::fast(),
1897 ..RetryConfig::default()
1898 }),
1899 rate_limit: None,
1900 ..Default::default()
1901 };
1902
1903 let client = HttpClientBuilder::with_config(config).build().unwrap();
1904 let url = format!("{}/retry-after", server.base_url());
1905
1906 let result = client.get(&url).send().await;
1907
1908 let response = result.expect("send() should return Ok(Response)");
1910 assert_eq!(response.status(), hyper::StatusCode::TOO_MANY_REQUESTS);
1911
1912 match response.error_for_status() {
1914 Err(HttpError::HttpStatus {
1915 status,
1916 retry_after,
1917 content_type,
1918 ..
1919 }) => {
1920 assert_eq!(status, hyper::StatusCode::TOO_MANY_REQUESTS);
1921 assert_eq!(
1922 retry_after,
1923 Some(std::time::Duration::from_mins(1)),
1924 "Should extract Retry-After header"
1925 );
1926 assert_eq!(
1927 content_type,
1928 Some("application/json".to_owned()),
1929 "Should extract Content-Type header"
1930 );
1931 }
1932 other => panic!("Expected HttpStatus error from error_for_status(), got: {other:?}"),
1933 }
1934 }
1935
1936 #[tokio::test]
1948 async fn test_status_retry_ignores_retry_after_when_configured() {
1949 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1950
1951 let server = MockServer::start();
1952 let mock = server.mock(|when, then| {
1953 when.method(Method::GET).path("/ignore-retry-after");
1954 then.status(429)
1955 .header("Retry-After", "10") .body("rate limited");
1957 });
1958
1959 let config = HttpClientConfig {
1960 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1961 retry: Some(RetryConfig {
1962 max_retries: 2,
1963 backoff: ExponentialBackoff::fast(), ignore_retry_after: true, ..RetryConfig::default()
1966 }),
1967 rate_limit: None,
1968 ..Default::default()
1969 };
1970
1971 let client = HttpClientBuilder::with_config(config).build().unwrap();
1972 let url = format!("{}/ignore-retry-after", server.base_url());
1973
1974 let start = std::time::Instant::now();
1975 let _result = client.get(&url).send().await;
1976 let elapsed = start.elapsed();
1977
1978 assert!(
1981 elapsed < std::time::Duration::from_secs(2),
1982 "Should have used fast backoff, not 10s Retry-After; elapsed: {elapsed:?}"
1983 );
1984
1985 assert_eq!(mock.calls(), 3, "Expected 3 calls, got {}", mock.calls());
1987 }
1988
1989 #[tokio::test]
1994 async fn test_non_retryable_status_passes_through() {
1995 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1996
1997 let server = MockServer::start();
1998 let mock = server.mock(|when, then| {
1999 when.method(Method::GET).path("/not-found");
2000 then.status(404)
2001 .header("content-type", "application/json")
2002 .body(r#"{"error": "not found"}"#);
2003 });
2004
2005 let config = HttpClientConfig {
2006 transport: crate::config::TransportSecurity::AllowInsecureHttp,
2007 retry: Some(RetryConfig {
2008 max_retries: 3,
2009 backoff: ExponentialBackoff::fast(),
2010 ..RetryConfig::default()
2011 }),
2012 rate_limit: None,
2013 ..Default::default()
2014 };
2015
2016 let client = HttpClientBuilder::with_config(config).build().unwrap();
2017 let url = format!("{}/not-found", server.base_url());
2018
2019 let result = client.get(&url).send().await;
2021
2022 assert_eq!(
2024 mock.calls(),
2025 1,
2026 "404 should not trigger retry; expected 1 call, got {}",
2027 mock.calls()
2028 );
2029
2030 let response = result.expect("send() should succeed for 404");
2032 assert_eq!(response.status(), hyper::StatusCode::NOT_FOUND);
2033
2034 }
2036
2037 #[tokio::test]
2042 async fn test_status_retry_exhausted_returns_ok_response() {
2043 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
2044
2045 let server = MockServer::start();
2046 let mock = server.mock(|when, then| {
2047 when.method(Method::GET).path("/always-500");
2048 then.status(500).body("always fails");
2049 });
2050
2051 let config = HttpClientConfig {
2052 transport: crate::config::TransportSecurity::AllowInsecureHttp,
2053 retry: Some(RetryConfig {
2054 max_retries: 2, backoff: ExponentialBackoff::fast(),
2056 ..RetryConfig::default()
2057 }),
2058 rate_limit: None,
2059 ..Default::default()
2060 };
2061
2062 let client = HttpClientBuilder::with_config(config).build().unwrap();
2063 let url = format!("{}/always-500", server.base_url());
2064
2065 let result = client.get(&url).send().await;
2066
2067 assert_eq!(
2069 mock.calls(),
2070 3,
2071 "Expected 3 calls (1 initial + 2 retries), got {}",
2072 mock.calls()
2073 );
2074
2075 let response = result.expect("send() should return Ok(Response) after retries exhaust");
2077 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
2078
2079 let err = response.error_for_status().unwrap_err();
2081 assert!(
2082 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
2083 );
2084 }
2085
2086 #[tokio::test]
2091 async fn test_no_retry_config_status_passes_through() {
2092 use crate::config::HttpClientConfig;
2093
2094 let server = MockServer::start();
2095 let mock = server.mock(|when, then| {
2096 when.method(Method::GET).path("/no-retry");
2097 then.status(500).body("server error");
2098 });
2099
2100 let config = HttpClientConfig {
2101 transport: crate::config::TransportSecurity::AllowInsecureHttp,
2102 retry: None, rate_limit: None,
2104 ..Default::default()
2105 };
2106
2107 let client = HttpClientBuilder::with_config(config).build().unwrap();
2108 let url = format!("{}/no-retry", server.base_url());
2109
2110 let result = client.get(&url).send().await;
2111
2112 assert_eq!(mock.calls(), 1);
2114
2115 let response = result.expect("send() should succeed when retry disabled");
2117 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
2118
2119 let err = response.error_for_status().unwrap_err();
2121 assert!(
2122 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
2123 );
2124 }
2125
2126 #[tokio::test]
2132 async fn test_url_scheme_http_rejected_with_tls_only() {
2133 let client = HttpClientBuilder::new()
2134 .transport(crate::config::TransportSecurity::TlsOnly)
2135 .retry(None)
2136 .build()
2137 .unwrap();
2138
2139 let result = client.get("http://example.com/test").send().await;
2141
2142 match result {
2144 Err(HttpError::InvalidScheme { scheme, reason }) => {
2145 assert_eq!(scheme, "http");
2146 assert!(
2147 reason.contains("TlsOnly"),
2148 "Error should mention TlsOnly: {reason}"
2149 );
2150 }
2151 Err(other) => panic!("Expected InvalidScheme error, got: {other:?}"),
2152 Ok(_) => panic!("Expected InvalidScheme error, but request succeeded"),
2153 }
2154 }
2155
2156 #[tokio::test]
2158 async fn test_url_scheme_http_allowed_with_allow_insecure() {
2159 let server = MockServer::start();
2160 let _m = server.mock(|when, then| {
2161 when.method(Method::GET).path("/test");
2162 then.status(200).body("ok");
2163 });
2164
2165 let client = HttpClientBuilder::new()
2166 .transport(crate::config::TransportSecurity::AllowInsecureHttp)
2167 .retry(None)
2168 .build()
2169 .unwrap();
2170
2171 let url = format!("{}/test", server.base_url()); let result = client.get(&url).send().await;
2173
2174 assert!(result.is_ok(), "http:// should be allowed: {result:?}");
2175 }
2176
2177 #[tokio::test]
2179 async fn test_url_scheme_https_always_allowed() {
2180 let client = HttpClientBuilder::new()
2183 .transport(crate::config::TransportSecurity::TlsOnly)
2184 .retry(None)
2185 .build()
2186 .unwrap();
2187
2188 let result = client.get("https://localhost:0/test").send().await;
2191
2192 if let Err(HttpError::InvalidScheme { .. }) = result {
2194 panic!("https:// should not trigger InvalidScheme error")
2195 }
2196 }
2198
2199 #[tokio::test]
2205 async fn test_url_scheme_invalid_rejected() {
2206 let client = HttpClientBuilder::new().retry(None).build().unwrap();
2207
2208 let result = client.get("ftp://files.example.com/file.txt").send().await;
2209
2210 match result {
2211 Err(HttpError::InvalidScheme { scheme, reason }) => {
2212 assert_eq!(scheme, "ftp");
2213 assert!(
2214 reason.contains("http://") || reason.contains("https://"),
2215 "Error should mention supported schemes: {reason}"
2216 );
2217 }
2218 Err(other) => panic!("Expected InvalidScheme error, got: {other:?}"),
2219 Ok(_) => panic!("Expected InvalidScheme error, but request succeeded"),
2220 }
2221 }
2222
2223 #[tokio::test]
2229 async fn test_url_scheme_missing_rejected() {
2230 let client = HttpClientBuilder::new().retry(None).build().unwrap();
2231
2232 let result = client.get("example.com/test").send().await;
2233
2234 match result {
2235 Err(HttpError::InvalidUri { url, reason, kind }) => {
2236 assert_eq!(url, "example.com/test");
2238 assert!(!reason.is_empty(), "Should have a reason for invalid URI");
2239 assert_eq!(kind, crate::error::InvalidUriKind::ParseError);
2240 }
2241 Err(other) => panic!("Expected InvalidUri error, got: {other:?}"),
2242 Ok(_) => panic!("Expected InvalidUri error, but request succeeded"),
2243 }
2244 }
2245}