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]
656 async fn test_small_buffer_capacity_no_deadlock() {
657 use crate::config::HttpClientConfig;
658
659 let server = MockServer::start();
660 let _m = server.mock(|when, then| {
661 when.method(Method::GET).path("/test");
662 then.status(200).body("ok");
663 });
664
665 let config = HttpClientConfig {
667 transport: crate::config::TransportSecurity::AllowInsecureHttp,
668 retry: None,
669 rate_limit: None,
670 buffer_capacity: 2,
671 ..Default::default()
672 };
673
674 let client = HttpClientBuilder::with_config(config).build().unwrap();
675 let url = format!("{}/test", server.base_url());
676
677 let handles: Vec<_> = (0..10)
679 .map(|_| {
680 let client = client.clone();
681 let url = url.clone();
682 tokio::spawn(async move { client.get(&url).send().await })
683 })
684 .collect();
685
686 let timeout_result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
688 let mut results = Vec::new();
689 for handle in handles {
690 results.push(handle.await);
691 }
692 results
693 })
694 .await;
695
696 let results = timeout_result.expect("requests should complete within timeout");
697
698 let mut success_count = 0;
699 let mut overloaded_count = 0;
700 for result in results {
701 match result.unwrap() {
702 Ok(resp) => {
703 assert_eq!(resp.status(), hyper::StatusCode::OK);
704 success_count += 1;
705 }
706 Err(HttpError::Overloaded) => {
707 overloaded_count += 1;
708 }
709 Err(e) => panic!("unexpected error: {e:?}"),
710 }
711 }
712
713 assert!(success_count > 0, "at least one request should succeed");
715 assert_eq!(success_count + overloaded_count, 10);
717 }
718
719 #[tokio::test]
724 async fn test_buffer_overflow_returns_overloaded() {
725 use crate::config::HttpClientConfig;
726
727 let server = MockServer::start();
728
729 let _m = server.mock(|when, then| {
730 when.method(Method::GET).path("/slow");
731 then.status(200).body("ok");
732 });
733
734 let config = HttpClientConfig {
736 transport: crate::config::TransportSecurity::AllowInsecureHttp,
737 retry: None,
738 rate_limit: None,
739 buffer_capacity: 1,
740 ..Default::default()
741 };
742
743 let client = HttpClientBuilder::with_config(config).build().unwrap();
744 let url = format!("{}/slow", server.base_url());
745
746 let client1 = client.clone();
748 let url1 = url.clone();
749 let handle1 = tokio::spawn(async move { client1.get(&url1).send().await });
750
751 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
753
754 let result2 = tokio::time::timeout(
756 std::time::Duration::from_millis(50),
757 client.get(&url).send(),
758 )
759 .await;
760
761 let inner_result = result2.expect("request should not timeout waiting for buffer");
763 match inner_result {
764 Err(HttpError::Overloaded) | Ok(_) => {}
766 Err(e) => panic!("unexpected error: {e:?}"),
767 }
768
769 _ = handle1.await;
771 }
772
773 #[tokio::test]
775 async fn test_large_body_no_deadlock() {
776 let server = MockServer::start();
777 let large_body = "x".repeat(100 * 1024); let _m = server.mock(|when, then| {
779 when.method(Method::GET).path("/large");
780 then.status(200).body(&large_body);
781 });
782
783 let client = HttpClientBuilder::new()
784 .retry(None)
785 .max_body_size(1024 * 1024) .build()
787 .unwrap();
788
789 let url = format!("{}/large", server.base_url());
790
791 let handles: Vec<_> = (0..5)
793 .map(|_| {
794 let client = client.clone();
795 let url = url.clone();
796 tokio::spawn(async move { client.get(&url).send().await?.checked_bytes().await })
797 })
798 .collect();
799
800 let timeout_result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
802 let mut results = Vec::new();
803 for handle in handles {
804 results.push(handle.await);
805 }
806 results
807 })
808 .await;
809
810 let results = timeout_result.expect("body reads should complete within timeout");
811 for result in results {
812 let body = result.unwrap().unwrap();
813 assert_eq!(body.len(), 100 * 1024);
814 }
815 }
816
817 #[tokio::test]
823 async fn test_token_endpoint_post_not_retried() {
824 use crate::config::HttpClientConfig;
825
826 let server = MockServer::start();
827
828 let mock = server.mock(|when, then| {
830 when.method(Method::POST).path("/token");
831 then.status(500).body("server error");
832 });
833
834 let mut config = HttpClientConfig::token_endpoint();
836 config.transport = crate::config::TransportSecurity::AllowInsecureHttp; let client = HttpClientBuilder::with_config(config).build().unwrap();
839 let url = format!("{}/token", server.base_url());
840
841 let result = client
843 .post(&url)
844 .form(&[("grant_type", "client_credentials"), ("client_id", "test")])
845 .unwrap()
846 .send()
847 .await;
848
849 assert!(result.is_ok()); let response = result.unwrap();
852 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
853
854 assert_eq!(
857 mock.calls(),
858 1,
859 "POST should not be retried; expected 1 call, got {}",
860 mock.calls()
861 );
862 }
863
864 #[tokio::test]
870 async fn test_http_client_put() {
871 let server = MockServer::start();
872 let _m = server.mock(|when, then| {
873 when.method(Method::PUT).path("/resource");
874 then.status(200).json_body(json!({"updated": true}));
875 });
876
877 let client = test_client();
878 let url = format!("{}/resource", server.base_url());
879 let resp = client.put(&url).send().await.unwrap();
880
881 assert_eq!(resp.status(), hyper::StatusCode::OK);
882 }
883
884 #[tokio::test]
885 async fn test_http_client_put_form() {
886 let server = MockServer::start();
887 let _m = server.mock(|when, then| {
888 when.method(Method::PUT)
889 .path("/resource")
890 .header("content-type", "application/x-www-form-urlencoded")
891 .body("name=updated&value=123");
892 then.status(200).json_body(json!({"updated": true}));
893 });
894
895 let client = test_client();
896 let url = format!("{}/resource", server.base_url());
897
898 let resp = client
899 .put(&url)
900 .form(&[("name", "updated"), ("value", "123")])
901 .unwrap()
902 .send()
903 .await
904 .unwrap();
905 assert_eq!(resp.status(), hyper::StatusCode::OK);
906 }
907
908 #[tokio::test]
909 async fn test_http_client_patch() {
910 let server = MockServer::start();
911 let _m = server.mock(|when, then| {
912 when.method(Method::PATCH).path("/resource/1");
913 then.status(200).json_body(json!({"patched": true}));
914 });
915
916 let client = test_client();
917 let url = format!("{}/resource/1", server.base_url());
918 let resp = client.patch(&url).send().await.unwrap();
919
920 assert_eq!(resp.status(), hyper::StatusCode::OK);
921 }
922
923 #[tokio::test]
924 async fn test_http_client_patch_form() {
925 let server = MockServer::start();
926 let _m = server.mock(|when, then| {
927 when.method(Method::PATCH)
928 .path("/resource/1")
929 .header("content-type", "application/x-www-form-urlencoded")
930 .body("field=patched");
931 then.status(200).json_body(json!({"patched": true}));
932 });
933
934 let client = test_client();
935 let url = format!("{}/resource/1", server.base_url());
936
937 let resp = client
938 .patch(&url)
939 .form(&[("field", "patched")])
940 .unwrap()
941 .send()
942 .await
943 .unwrap();
944 assert_eq!(resp.status(), hyper::StatusCode::OK);
945 }
946
947 #[tokio::test]
948 async fn test_http_client_delete() {
949 let server = MockServer::start();
950 let _m = server.mock(|when, then| {
951 when.method(Method::DELETE).path("/resource/42");
952 then.status(204);
953 });
954
955 let client = test_client();
956 let url = format!("{}/resource/42", server.base_url());
957 let resp = client.delete(&url).send().await.unwrap();
958
959 assert_eq!(resp.status(), hyper::StatusCode::NO_CONTENT);
960 }
961
962 #[tokio::test]
963 async fn test_http_client_delete_returns_200() {
964 let server = MockServer::start();
965 let _m = server.mock(|when, then| {
966 when.method(Method::DELETE).path("/resource/99");
967 then.status(200).json_body(json!({"deleted": true}));
968 });
969
970 let client = test_client();
971 let url = format!("{}/resource/99", server.base_url());
972 let resp = client.delete(&url).send().await.unwrap();
973
974 assert_eq!(resp.status(), hyper::StatusCode::OK);
975 }
976
977 #[tokio::test]
978 async fn test_http_client_head_returns_headers_no_body() {
979 let server = MockServer::start();
980 let _m = server.mock(|when, then| {
981 when.method(Method::HEAD).path("/probe");
982 then.status(200)
983 .header("content-length", "12345")
984 .header("etag", "\"abc\"");
985 });
986
987 let client = test_client();
988 let url = format!("{}/probe", server.base_url());
989 let resp = client.head(&url).send().await.unwrap();
990
991 assert_eq!(resp.status(), hyper::StatusCode::OK);
992 assert_eq!(
993 resp.headers()
994 .get("content-length")
995 .and_then(|v| v.to_str().ok()),
996 Some("12345"),
997 );
998 let body = resp.bytes().await.unwrap();
999 assert!(body.is_empty(), "HEAD response body must be empty");
1000 }
1001
1002 #[tokio::test]
1003 async fn test_http_client_options_returns_allow_header() {
1004 let server = MockServer::start();
1005 let _m = server.mock(|when, then| {
1006 when.method(Method::OPTIONS).path("/resource");
1007 then.status(204).header("allow", "GET, POST, PUT, DELETE");
1008 });
1009
1010 let client = test_client();
1011 let url = format!("{}/resource", server.base_url());
1012 let resp = client.options(&url).send().await.unwrap();
1013
1014 assert_eq!(resp.status(), hyper::StatusCode::NO_CONTENT);
1015 assert_eq!(
1016 resp.headers().get("allow").and_then(|v| v.to_str().ok()),
1017 Some("GET, POST, PUT, DELETE"),
1018 );
1019 }
1020
1021 #[tokio::test]
1022 async fn test_put_form_with_custom_headers() {
1023 let server = MockServer::start();
1024 let _m = server.mock(|when, then| {
1025 when.method(Method::PUT)
1026 .path("/api/data")
1027 .header("content-type", "application/x-www-form-urlencoded")
1028 .header("x-custom-header", "custom-value")
1029 .body("key=value");
1030 then.status(200);
1031 });
1032
1033 let client = test_client();
1034 let url = format!("{}/api/data", server.base_url());
1035
1036 let resp = client
1037 .put(&url)
1038 .header("x-custom-header", "custom-value")
1039 .form(&[("key", "value")])
1040 .unwrap()
1041 .send()
1042 .await
1043 .unwrap();
1044 assert_eq!(resp.status(), hyper::StatusCode::OK);
1045 }
1046
1047 #[tokio::test]
1048 async fn test_patch_form_with_custom_headers() {
1049 let server = MockServer::start();
1050 let _m = server.mock(|when, then| {
1051 when.method(Method::PATCH)
1052 .path("/api/data")
1053 .header("content-type", "application/x-www-form-urlencoded")
1054 .header("authorization", "Bearer token123")
1055 .body("status=active");
1056 then.status(200);
1057 });
1058
1059 let client = test_client();
1060 let url = format!("{}/api/data", server.base_url());
1061
1062 let resp = client
1063 .patch(&url)
1064 .header("authorization", "Bearer token123")
1065 .form(&[("status", "active")])
1066 .unwrap()
1067 .send()
1068 .await
1069 .unwrap();
1070 assert_eq!(resp.status(), hyper::StatusCode::OK);
1071 }
1072
1073 #[tokio::test]
1074 async fn test_request_builder_json_body() {
1075 #[derive(serde::Serialize)]
1076 struct CreateUser {
1077 name: String,
1078 email: String,
1079 }
1080
1081 let server = MockServer::start();
1082 let _m = server.mock(|when, then| {
1083 when.method(Method::POST)
1084 .path("/users")
1085 .header("content-type", "application/json")
1086 .json_body(json!({"name": "Alice", "email": "alice@example.com"}));
1087 then.status(201).json_body(json!({"id": 1}));
1088 });
1089
1090 let client = test_client();
1091 let url = format!("{}/users", server.base_url());
1092
1093 let resp = client
1094 .post(&url)
1095 .json(&CreateUser {
1096 name: "Alice".into(),
1097 email: "alice@example.com".into(),
1098 })
1099 .unwrap()
1100 .send()
1101 .await
1102 .unwrap();
1103 assert_eq!(resp.status(), hyper::StatusCode::CREATED);
1104 }
1105
1106 #[tokio::test]
1107 async fn test_request_builder_body_bytes() {
1108 let server = MockServer::start();
1109 let _m = server.mock(|when, then| {
1110 when.method(Method::POST)
1111 .path("/upload")
1112 .body("raw binary data");
1113 then.status(200);
1114 });
1115
1116 let client = test_client();
1117 let url = format!("{}/upload", server.base_url());
1118
1119 let resp = client
1120 .post(&url)
1121 .body_bytes(bytes::Bytes::from("raw binary data"))
1122 .send()
1123 .await
1124 .unwrap();
1125 assert_eq!(resp.status(), hyper::StatusCode::OK);
1126 }
1127
1128 #[tokio::test]
1134 async fn test_content_type_not_duplicated_with_json() {
1135 #[derive(serde::Serialize)]
1136 struct TestData {
1137 value: i32,
1138 }
1139
1140 let server = MockServer::start();
1141 let mock = server.mock(|when, then| {
1142 when.method(Method::POST)
1143 .path("/custom-content-type")
1144 .header("content-type", "application/vnd.custom+json");
1146 then.status(200);
1147 });
1148
1149 let client = test_client();
1150 let url = format!("{}/custom-content-type", server.base_url());
1151
1152 let resp = client
1153 .post(&url)
1154 .header("content-type", "application/vnd.custom+json") .json(&TestData { value: 42 })
1156 .unwrap()
1157 .send()
1158 .await
1159 .unwrap();
1160
1161 assert_eq!(resp.status(), hyper::StatusCode::OK);
1162 assert_eq!(
1163 mock.calls(),
1164 1,
1165 "Request with custom Content-Type should match"
1166 );
1167 }
1168
1169 #[tokio::test]
1171 async fn test_content_type_not_duplicated_with_form() {
1172 let server = MockServer::start();
1173 let mock = server.mock(|when, then| {
1174 when.method(Method::POST)
1175 .path("/custom-form-type")
1176 .header("content-type", "application/x-custom-form");
1178 then.status(200);
1179 });
1180
1181 let client = test_client();
1182 let url = format!("{}/custom-form-type", server.base_url());
1183
1184 let resp = client
1185 .post(&url)
1186 .header("content-type", "application/x-custom-form") .form(&[("key", "value")])
1188 .unwrap()
1189 .send()
1190 .await
1191 .unwrap();
1192
1193 assert_eq!(resp.status(), hyper::StatusCode::OK);
1194 assert_eq!(
1195 mock.calls(),
1196 1,
1197 "Request with custom Content-Type should match"
1198 );
1199 }
1200
1201 #[tokio::test]
1202 async fn test_request_builder_body_string() {
1203 let server = MockServer::start();
1204 let _m = server.mock(|when, then| {
1205 when.method(Method::POST)
1206 .path("/text")
1207 .body("Hello, World!");
1208 then.status(200);
1209 });
1210
1211 let client = test_client();
1212 let url = format!("{}/text", server.base_url());
1213
1214 let resp = client
1215 .post(&url)
1216 .body_string("Hello, World!".into())
1217 .send()
1218 .await
1219 .unwrap();
1220 assert_eq!(resp.status(), hyper::StatusCode::OK);
1221 }
1222
1223 #[tokio::test]
1224 async fn test_response_text_method() {
1225 let server = MockServer::start();
1226 let _m = server.mock(|when, then| {
1227 when.method(Method::GET).path("/text");
1228 then.status(200).body("Hello, World!");
1229 });
1230
1231 let client = test_client();
1232 let url = format!("{}/text", server.base_url());
1233
1234 let text = client.get(&url).send().await.unwrap().text().await.unwrap();
1235 assert_eq!(text, "Hello, World!");
1236 }
1237
1238 #[tokio::test]
1239 async fn test_request_builder_multiple_headers() {
1240 let server = MockServer::start();
1241 let _m = server.mock(|when, then| {
1242 when.method(Method::GET)
1243 .path("/headers")
1244 .header("x-first", "one")
1245 .header("x-second", "two");
1246 then.status(200);
1247 });
1248
1249 let client = test_client();
1250 let url = format!("{}/headers", server.base_url());
1251
1252 let resp = client
1253 .get(&url)
1254 .header("x-first", "one")
1255 .header("x-second", "two")
1256 .send()
1257 .await
1258 .unwrap();
1259 assert_eq!(resp.status(), hyper::StatusCode::OK);
1260 }
1261
1262 #[tokio::test]
1263 async fn test_request_builder_headers_vec() {
1264 let server = MockServer::start();
1265 let _m = server.mock(|when, then| {
1266 when.method(Method::GET)
1267 .path("/headers")
1268 .header("x-first", "one")
1269 .header("x-second", "two");
1270 then.status(200);
1271 });
1272
1273 let client = test_client();
1274 let url = format!("{}/headers", server.base_url());
1275
1276 let resp = client
1277 .get(&url)
1278 .headers(vec![
1279 ("x-first".to_owned(), "one".to_owned()),
1280 ("x-second".to_owned(), "two".to_owned()),
1281 ])
1282 .send()
1283 .await
1284 .unwrap();
1285 assert_eq!(resp.status(), hyper::StatusCode::OK);
1286 }
1287
1288 #[tokio::test]
1291 async fn test_error_response_with_large_body_returns_http_status() {
1292 use crate::security::ERROR_BODY_PREVIEW_LIMIT;
1293
1294 let server = MockServer::start();
1295
1296 let large_body = "x".repeat(ERROR_BODY_PREVIEW_LIMIT + 1000);
1298
1299 let _m = server.mock(|when, then| {
1300 when.method(Method::GET).path("/error-with-large-body");
1301 then.status(500).body(&large_body);
1302 });
1303
1304 let client = test_client();
1305 let url = format!("{}/error-with-large-body", server.base_url());
1306
1307 let result = client.get(&url).send().await.unwrap().checked_bytes().await;
1308
1309 match result {
1311 Err(HttpError::HttpStatus {
1312 status,
1313 body_preview,
1314 ..
1315 }) => {
1316 assert_eq!(status, hyper::StatusCode::INTERNAL_SERVER_ERROR);
1317 assert_eq!(body_preview, "<body too large for preview>");
1319 }
1320 Err(HttpError::BodyTooLarge { .. }) => {
1321 panic!("Should return HttpStatus, not BodyTooLarge for non-2xx responses");
1322 }
1323 Err(other) => panic!("Unexpected error: {other:?}"),
1324 Ok(_) => panic!("Should have returned an error for 500 status"),
1325 }
1326 }
1327
1328 fn gzip_compress(data: &[u8]) -> Vec<u8> {
1334 use flate2::Compression;
1335 use flate2::write::GzEncoder;
1336 use std::io::Write;
1337
1338 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1339 encoder.write_all(data).unwrap();
1340 encoder.finish().unwrap()
1341 }
1342
1343 #[tokio::test]
1348 async fn test_gzip_decompression_basic() {
1349 let server = MockServer::start();
1350
1351 let original_body = b"Hello, this is a test body that will be gzip compressed!";
1352 let compressed_body = gzip_compress(original_body);
1353
1354 let _m = server.mock(|when, then| {
1355 when.method(Method::GET).path("/gzip");
1356 then.status(200)
1357 .header("content-encoding", "gzip")
1358 .body(compressed_body);
1359 });
1360
1361 let client = test_client();
1362 let url = format!("{}/gzip", server.base_url());
1363
1364 let body = client
1365 .get(&url)
1366 .send()
1367 .await
1368 .unwrap()
1369 .bytes()
1370 .await
1371 .unwrap();
1372
1373 assert_eq!(
1374 body.as_ref(),
1375 original_body,
1376 "Decompressed body should match original"
1377 );
1378 }
1379
1380 #[tokio::test]
1385 async fn test_gzip_decompression_json() {
1386 #[derive(serde::Deserialize, PartialEq, Debug)]
1387 struct TestData {
1388 name: String,
1389 value: i32,
1390 nested: NestedData,
1391 }
1392
1393 #[derive(serde::Deserialize, PartialEq, Debug)]
1394 struct NestedData {
1395 items: Vec<String>,
1396 }
1397
1398 let server = MockServer::start();
1399
1400 let json_body = r#"{"name":"test","value":42,"nested":{"items":["a","b","c"]}}"#;
1401 let compressed_body = gzip_compress(json_body.as_bytes());
1402
1403 let _m = server.mock(|when, then| {
1404 when.method(Method::GET).path("/gzip-json");
1405 then.status(200)
1406 .header("content-type", "application/json")
1407 .header("content-encoding", "gzip")
1408 .body(compressed_body);
1409 });
1410
1411 let client = test_client();
1412 let url = format!("{}/gzip-json", server.base_url());
1413
1414 let data: TestData = client.get(&url).send().await.unwrap().json().await.unwrap();
1415
1416 assert_eq!(data.name, "test");
1417 assert_eq!(data.value, 42);
1418 assert_eq!(data.nested.items, vec!["a", "b", "c"]);
1419 }
1420
1421 #[tokio::test]
1429 async fn test_gzip_decompression_body_size_limit() {
1430 let server = MockServer::start();
1431
1432 let large_decompressed = vec![b'x'; 100 * 1024]; let compressed_body = gzip_compress(&large_decompressed);
1436
1437 assert!(
1439 compressed_body.len() < 2000,
1440 "Compressed body should be small (got {} bytes)",
1441 compressed_body.len()
1442 );
1443
1444 let _m = server.mock(|when, then| {
1445 when.method(Method::GET).path("/gzip-bomb");
1446 then.status(200)
1447 .header("content-encoding", "gzip")
1448 .body(compressed_body);
1449 });
1450
1451 let client = HttpClientBuilder::new()
1453 .retry(None)
1454 .max_body_size(10 * 1024) .build()
1456 .unwrap();
1457
1458 let url = format!("{}/gzip-bomb", server.base_url());
1459 let result = client.get(&url).send().await.unwrap().bytes().await;
1460
1461 match result {
1463 Err(HttpError::BodyTooLarge { limit, actual }) => {
1464 assert_eq!(limit, 10 * 1024, "Limit should be 10KB");
1465 assert!(
1466 actual > limit,
1467 "Actual size ({actual}) should exceed limit ({limit})"
1468 );
1469 }
1470 Err(other) => panic!("Expected BodyTooLarge error, got: {other:?}"),
1471 Ok(body) => panic!(
1472 "Expected BodyTooLarge error, but got {} bytes of body",
1473 body.len()
1474 ),
1475 }
1476 }
1477
1478 #[tokio::test]
1483 async fn test_accept_encoding_header_sent() {
1484 let server = MockServer::start();
1485
1486 let mock = server.mock(|when, then| {
1488 when.method(Method::GET)
1489 .path("/check-accept-encoding")
1490 .header_exists("accept-encoding");
1491 then.status(200).body("ok");
1492 });
1493
1494 let client = test_client();
1495 let url = format!("{}/check-accept-encoding", server.base_url());
1496
1497 let resp = client.get(&url).send().await.unwrap();
1498 assert_eq!(resp.status(), hyper::StatusCode::OK);
1499
1500 assert_eq!(
1502 mock.calls(),
1503 1,
1504 "Request should have included Accept-Encoding header"
1505 );
1506 }
1507
1508 #[tokio::test]
1512 async fn test_no_compression_passthrough() {
1513 let server = MockServer::start();
1514
1515 let plain_body = b"This is plain text, not compressed";
1516
1517 let _m = server.mock(|when, then| {
1518 when.method(Method::GET).path("/plain");
1519 then.status(200)
1520 .header("content-type", "text/plain")
1521 .body(plain_body.as_slice());
1522 });
1523
1524 let client = test_client();
1525 let url = format!("{}/plain", server.base_url());
1526
1527 let body = client
1528 .get(&url)
1529 .send()
1530 .await
1531 .unwrap()
1532 .bytes()
1533 .await
1534 .unwrap();
1535
1536 assert_eq!(
1537 body.as_ref(),
1538 plain_body,
1539 "Plain body should pass through unchanged"
1540 );
1541 }
1542
1543 #[tokio::test]
1545 async fn test_gzip_decompression_checked_bytes() {
1546 let server = MockServer::start();
1547
1548 let original_body = b"Checked bytes test with gzip";
1549 let compressed_body = gzip_compress(original_body);
1550
1551 let _m = server.mock(|when, then| {
1552 when.method(Method::GET).path("/gzip-checked");
1553 then.status(200)
1554 .header("content-encoding", "gzip")
1555 .body(compressed_body);
1556 });
1557
1558 let client = test_client();
1559 let url = format!("{}/gzip-checked", server.base_url());
1560
1561 let body = client
1562 .get(&url)
1563 .send()
1564 .await
1565 .unwrap()
1566 .checked_bytes()
1567 .await
1568 .unwrap();
1569
1570 assert_eq!(
1571 body.as_ref(),
1572 original_body,
1573 "checked_bytes should return decompressed content"
1574 );
1575 }
1576
1577 #[tokio::test]
1579 async fn test_gzip_decompression_text() {
1580 let server = MockServer::start();
1581
1582 let original_text = "Hello, World! \u{1F600}"; let compressed_body = gzip_compress(original_text.as_bytes());
1584
1585 let _m = server.mock(|when, then| {
1586 when.method(Method::GET).path("/gzip-text");
1587 then.status(200)
1588 .header("content-type", "text/plain; charset=utf-8")
1589 .header("content-encoding", "gzip")
1590 .body(compressed_body);
1591 });
1592
1593 let client = test_client();
1594 let url = format!("{}/gzip-text", server.base_url());
1595
1596 let text = client.get(&url).send().await.unwrap().text().await.unwrap();
1597
1598 assert_eq!(
1599 text, original_text,
1600 "text() should return decompressed UTF-8 content"
1601 );
1602 }
1603
1604 #[test]
1610 fn test_map_buffer_error_passes_through_http_error() {
1611 let http_err = HttpError::Timeout(std::time::Duration::from_secs(10));
1612 let boxed: tower::BoxError = Box::new(http_err);
1613 let result = map_buffer_error(boxed);
1614
1615 assert!(
1616 matches!(result, HttpError::Timeout(_)),
1617 "Should pass through HttpError::Timeout, got: {result:?}"
1618 );
1619 }
1620
1621 #[test]
1626 fn test_map_buffer_error_returns_service_closed_for_unknown_error() {
1627 let other_err: tower::BoxError = Box::new(std::io::Error::new(
1629 std::io::ErrorKind::BrokenPipe,
1630 "buffer worker died",
1631 ));
1632 let result = map_buffer_error(other_err);
1633
1634 assert!(
1635 matches!(result, HttpError::ServiceClosed),
1636 "Should return ServiceClosed for non-HttpError, got: {result:?}"
1637 );
1638 }
1639
1640 #[tokio::test]
1657 async fn test_status_retry_get_500_retried() {
1658 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1659
1660 let server = MockServer::start();
1661 let mock = server.mock(|when, then| {
1662 when.method(Method::GET).path("/retry-500");
1663 then.status(500).body("server error");
1664 });
1665
1666 let config = HttpClientConfig {
1667 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1668 retry: Some(RetryConfig {
1669 max_retries: 2, backoff: ExponentialBackoff::fast(),
1671 ..RetryConfig::default() }),
1673 rate_limit: None,
1674 ..Default::default()
1675 };
1676
1677 let client = HttpClientBuilder::with_config(config).build().unwrap();
1678 let url = format!("{}/retry-500", server.base_url());
1679
1680 let result = client.get(&url).send().await;
1681
1682 assert_eq!(
1684 mock.calls(),
1685 3,
1686 "GET should retry on 500; expected 3 calls (1 + 2 retries), got {}",
1687 mock.calls()
1688 );
1689
1690 let response = result.expect("send() should return Ok(Response) after retries exhaust");
1692 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
1693
1694 let err = response.error_for_status().unwrap_err();
1696 assert!(
1697 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
1698 );
1699 }
1700
1701 #[tokio::test]
1710 async fn test_status_retry_post_500_not_retried() {
1711 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1712
1713 let server = MockServer::start();
1714 let mock = server.mock(|when, then| {
1715 when.method(Method::POST).path("/post-500");
1716 then.status(500).body("server error");
1717 });
1718
1719 let config = HttpClientConfig {
1720 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1721 retry: Some(RetryConfig {
1722 max_retries: 3,
1723 backoff: ExponentialBackoff::fast(),
1724 ..RetryConfig::default() }),
1726 rate_limit: None,
1727 ..Default::default()
1728 };
1729
1730 let client = HttpClientBuilder::with_config(config).build().unwrap();
1731 let url = format!("{}/post-500", server.base_url());
1732
1733 let result = client.post(&url).send().await;
1734
1735 assert_eq!(
1737 mock.calls(),
1738 1,
1739 "POST should not be retried on 500; expected 1 call, got {}",
1740 mock.calls()
1741 );
1742
1743 let response = result.expect("POST + 500 should return Ok(Response), not Err");
1746 assert_eq!(
1747 response.status(),
1748 hyper::StatusCode::INTERNAL_SERVER_ERROR,
1749 "Response should have 500 status"
1750 );
1751
1752 }
1754
1755 #[tokio::test]
1761 async fn test_status_retry_post_429_retried() {
1762 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1763
1764 let server = MockServer::start();
1765 let mock = server.mock(|when, then| {
1766 when.method(Method::POST).path("/post-429");
1767 then.status(429).body("rate limited");
1768 });
1769
1770 let config = HttpClientConfig {
1771 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1772 retry: Some(RetryConfig {
1773 max_retries: 2, backoff: ExponentialBackoff::fast(),
1775 ..RetryConfig::default() }),
1777 rate_limit: None,
1778 ..Default::default()
1779 };
1780
1781 let client = HttpClientBuilder::with_config(config).build().unwrap();
1782 let url = format!("{}/post-429", server.base_url());
1783
1784 let result = client.post(&url).send().await;
1785
1786 assert_eq!(
1788 mock.calls(),
1789 3,
1790 "POST should retry on 429; expected 3 calls (1 + 2 retries), got {}",
1791 mock.calls()
1792 );
1793
1794 let response = result.expect("send() should return Ok(Response) after retries exhaust");
1796 assert_eq!(response.status(), hyper::StatusCode::TOO_MANY_REQUESTS);
1797
1798 let err = response.error_for_status().unwrap_err();
1800 assert!(
1801 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::TOO_MANY_REQUESTS)
1802 );
1803 }
1804
1805 #[tokio::test]
1810 async fn test_status_retry_extracts_retry_after_header() {
1811 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1812
1813 let server = MockServer::start();
1814 let _mock = server.mock(|when, then| {
1815 when.method(Method::GET).path("/retry-after");
1816 then.status(429)
1817 .header("Retry-After", "60")
1818 .header("Content-Type", "application/json")
1819 .body(r#"{"error": "rate limited"}"#);
1820 });
1821
1822 let config = HttpClientConfig {
1823 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1824 retry: Some(RetryConfig {
1825 max_retries: 0, backoff: ExponentialBackoff::fast(),
1827 ..RetryConfig::default()
1828 }),
1829 rate_limit: None,
1830 ..Default::default()
1831 };
1832
1833 let client = HttpClientBuilder::with_config(config).build().unwrap();
1834 let url = format!("{}/retry-after", server.base_url());
1835
1836 let result = client.get(&url).send().await;
1837
1838 let response = result.expect("send() should return Ok(Response)");
1840 assert_eq!(response.status(), hyper::StatusCode::TOO_MANY_REQUESTS);
1841
1842 match response.error_for_status() {
1844 Err(HttpError::HttpStatus {
1845 status,
1846 retry_after,
1847 content_type,
1848 ..
1849 }) => {
1850 assert_eq!(status, hyper::StatusCode::TOO_MANY_REQUESTS);
1851 assert_eq!(
1852 retry_after,
1853 Some(std::time::Duration::from_mins(1)),
1854 "Should extract Retry-After header"
1855 );
1856 assert_eq!(
1857 content_type,
1858 Some("application/json".to_owned()),
1859 "Should extract Content-Type header"
1860 );
1861 }
1862 other => panic!("Expected HttpStatus error from error_for_status(), got: {other:?}"),
1863 }
1864 }
1865
1866 #[tokio::test]
1878 async fn test_status_retry_ignores_retry_after_when_configured() {
1879 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1880
1881 let server = MockServer::start();
1882 let mock = server.mock(|when, then| {
1883 when.method(Method::GET).path("/ignore-retry-after");
1884 then.status(429)
1885 .header("Retry-After", "10") .body("rate limited");
1887 });
1888
1889 let config = HttpClientConfig {
1890 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1891 retry: Some(RetryConfig {
1892 max_retries: 2,
1893 backoff: ExponentialBackoff::fast(), ignore_retry_after: true, ..RetryConfig::default()
1896 }),
1897 rate_limit: None,
1898 ..Default::default()
1899 };
1900
1901 let client = HttpClientBuilder::with_config(config).build().unwrap();
1902 let url = format!("{}/ignore-retry-after", server.base_url());
1903
1904 let start = std::time::Instant::now();
1905 let _result = client.get(&url).send().await;
1906 let elapsed = start.elapsed();
1907
1908 assert!(
1911 elapsed < std::time::Duration::from_secs(2),
1912 "Should have used fast backoff, not 10s Retry-After; elapsed: {elapsed:?}"
1913 );
1914
1915 assert_eq!(mock.calls(), 3, "Expected 3 calls, got {}", mock.calls());
1917 }
1918
1919 #[tokio::test]
1924 async fn test_non_retryable_status_passes_through() {
1925 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1926
1927 let server = MockServer::start();
1928 let mock = server.mock(|when, then| {
1929 when.method(Method::GET).path("/not-found");
1930 then.status(404)
1931 .header("content-type", "application/json")
1932 .body(r#"{"error": "not found"}"#);
1933 });
1934
1935 let config = HttpClientConfig {
1936 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1937 retry: Some(RetryConfig {
1938 max_retries: 3,
1939 backoff: ExponentialBackoff::fast(),
1940 ..RetryConfig::default()
1941 }),
1942 rate_limit: None,
1943 ..Default::default()
1944 };
1945
1946 let client = HttpClientBuilder::with_config(config).build().unwrap();
1947 let url = format!("{}/not-found", server.base_url());
1948
1949 let result = client.get(&url).send().await;
1951
1952 assert_eq!(
1954 mock.calls(),
1955 1,
1956 "404 should not trigger retry; expected 1 call, got {}",
1957 mock.calls()
1958 );
1959
1960 let response = result.expect("send() should succeed for 404");
1962 assert_eq!(response.status(), hyper::StatusCode::NOT_FOUND);
1963
1964 }
1966
1967 #[tokio::test]
1972 async fn test_status_retry_exhausted_returns_ok_response() {
1973 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1974
1975 let server = MockServer::start();
1976 let mock = server.mock(|when, then| {
1977 when.method(Method::GET).path("/always-500");
1978 then.status(500).body("always fails");
1979 });
1980
1981 let config = HttpClientConfig {
1982 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1983 retry: Some(RetryConfig {
1984 max_retries: 2, backoff: ExponentialBackoff::fast(),
1986 ..RetryConfig::default()
1987 }),
1988 rate_limit: None,
1989 ..Default::default()
1990 };
1991
1992 let client = HttpClientBuilder::with_config(config).build().unwrap();
1993 let url = format!("{}/always-500", server.base_url());
1994
1995 let result = client.get(&url).send().await;
1996
1997 assert_eq!(
1999 mock.calls(),
2000 3,
2001 "Expected 3 calls (1 initial + 2 retries), got {}",
2002 mock.calls()
2003 );
2004
2005 let response = result.expect("send() should return Ok(Response) after retries exhaust");
2007 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
2008
2009 let err = response.error_for_status().unwrap_err();
2011 assert!(
2012 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
2013 );
2014 }
2015
2016 #[tokio::test]
2021 async fn test_no_retry_config_status_passes_through() {
2022 use crate::config::HttpClientConfig;
2023
2024 let server = MockServer::start();
2025 let mock = server.mock(|when, then| {
2026 when.method(Method::GET).path("/no-retry");
2027 then.status(500).body("server error");
2028 });
2029
2030 let config = HttpClientConfig {
2031 transport: crate::config::TransportSecurity::AllowInsecureHttp,
2032 retry: None, rate_limit: None,
2034 ..Default::default()
2035 };
2036
2037 let client = HttpClientBuilder::with_config(config).build().unwrap();
2038 let url = format!("{}/no-retry", server.base_url());
2039
2040 let result = client.get(&url).send().await;
2041
2042 assert_eq!(mock.calls(), 1);
2044
2045 let response = result.expect("send() should succeed when retry disabled");
2047 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
2048
2049 let err = response.error_for_status().unwrap_err();
2051 assert!(
2052 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
2053 );
2054 }
2055
2056 #[tokio::test]
2062 async fn test_url_scheme_http_rejected_with_tls_only() {
2063 let client = HttpClientBuilder::new()
2064 .transport(crate::config::TransportSecurity::TlsOnly)
2065 .retry(None)
2066 .build()
2067 .unwrap();
2068
2069 let result = client.get("http://example.com/test").send().await;
2071
2072 match result {
2074 Err(HttpError::InvalidScheme { scheme, reason }) => {
2075 assert_eq!(scheme, "http");
2076 assert!(
2077 reason.contains("TlsOnly"),
2078 "Error should mention TlsOnly: {reason}"
2079 );
2080 }
2081 Err(other) => panic!("Expected InvalidScheme error, got: {other:?}"),
2082 Ok(_) => panic!("Expected InvalidScheme error, but request succeeded"),
2083 }
2084 }
2085
2086 #[tokio::test]
2088 async fn test_url_scheme_http_allowed_with_allow_insecure() {
2089 let server = MockServer::start();
2090 let _m = server.mock(|when, then| {
2091 when.method(Method::GET).path("/test");
2092 then.status(200).body("ok");
2093 });
2094
2095 let client = HttpClientBuilder::new()
2096 .transport(crate::config::TransportSecurity::AllowInsecureHttp)
2097 .retry(None)
2098 .build()
2099 .unwrap();
2100
2101 let url = format!("{}/test", server.base_url()); let result = client.get(&url).send().await;
2103
2104 assert!(result.is_ok(), "http:// should be allowed: {result:?}");
2105 }
2106
2107 #[tokio::test]
2109 async fn test_url_scheme_https_always_allowed() {
2110 let client = HttpClientBuilder::new()
2113 .transport(crate::config::TransportSecurity::TlsOnly)
2114 .retry(None)
2115 .build()
2116 .unwrap();
2117
2118 let result = client.get("https://localhost:0/test").send().await;
2121
2122 if let Err(HttpError::InvalidScheme { .. }) = result {
2124 panic!("https:// should not trigger InvalidScheme error")
2125 }
2126 }
2128
2129 #[tokio::test]
2135 async fn test_url_scheme_invalid_rejected() {
2136 let client = HttpClientBuilder::new().retry(None).build().unwrap();
2137
2138 let result = client.get("ftp://files.example.com/file.txt").send().await;
2139
2140 match result {
2141 Err(HttpError::InvalidScheme { scheme, reason }) => {
2142 assert_eq!(scheme, "ftp");
2143 assert!(
2144 reason.contains("http://") || reason.contains("https://"),
2145 "Error should mention supported schemes: {reason}"
2146 );
2147 }
2148 Err(other) => panic!("Expected InvalidScheme error, got: {other:?}"),
2149 Ok(_) => panic!("Expected InvalidScheme error, but request succeeded"),
2150 }
2151 }
2152
2153 #[tokio::test]
2159 async fn test_url_scheme_missing_rejected() {
2160 let client = HttpClientBuilder::new().retry(None).build().unwrap();
2161
2162 let result = client.get("example.com/test").send().await;
2163
2164 match result {
2165 Err(HttpError::InvalidUri { url, reason, kind }) => {
2166 assert_eq!(url, "example.com/test");
2168 assert!(!reason.is_empty(), "Should have a reason for invalid URI");
2169 assert_eq!(kind, crate::error::InvalidUriKind::ParseError);
2170 }
2171 Err(other) => panic!("Expected InvalidUri error, got: {other:?}"),
2172 Ok(_) => panic!("Expected InvalidUri error, but request succeeded"),
2173 }
2174 }
2175}