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,
63 pub(crate) max_body_size: usize,
64 pub(crate) transport_security: TransportSecurity,
65}
66
67impl HttpClient {
68 pub fn new() -> Result<Self, HttpError> {
73 HttpClientBuilder::new().build()
74 }
75
76 #[must_use]
78 pub fn builder() -> HttpClientBuilder {
79 HttpClientBuilder::new()
80 }
81
82 pub fn get(&self, url: &str) -> RequestBuilder {
134 RequestBuilder::new(
135 self.service.clone(),
136 self.max_body_size,
137 http::Method::GET,
138 url.to_owned(),
139 self.transport_security,
140 )
141 }
142
143 pub fn post(&self, url: &str) -> RequestBuilder {
173 RequestBuilder::new(
174 self.service.clone(),
175 self.max_body_size,
176 http::Method::POST,
177 url.to_owned(),
178 self.transport_security,
179 )
180 }
181
182 pub fn put(&self, url: &str) -> RequestBuilder {
204 RequestBuilder::new(
205 self.service.clone(),
206 self.max_body_size,
207 http::Method::PUT,
208 url.to_owned(),
209 self.transport_security,
210 )
211 }
212
213 pub fn patch(&self, url: &str) -> RequestBuilder {
235 RequestBuilder::new(
236 self.service.clone(),
237 self.max_body_size,
238 http::Method::PATCH,
239 url.to_owned(),
240 self.transport_security,
241 )
242 }
243
244 pub fn delete(&self, url: &str) -> RequestBuilder {
264 RequestBuilder::new(
265 self.service.clone(),
266 self.max_body_size,
267 http::Method::DELETE,
268 url.to_owned(),
269 self.transport_security,
270 )
271 }
272
273 pub fn head(&self, url: &str) -> RequestBuilder {
299 RequestBuilder::new(
300 self.service.clone(),
301 self.max_body_size,
302 http::Method::HEAD,
303 url.to_owned(),
304 self.transport_security,
305 )
306 }
307
308 pub fn options(&self, url: &str) -> RequestBuilder {
337 RequestBuilder::new(
338 self.service.clone(),
339 self.max_body_size,
340 http::Method::OPTIONS,
341 url.to_owned(),
342 self.transport_security,
343 )
344 }
345}
346
347pub fn map_buffer_error(err: tower::BoxError) -> HttpError {
352 match err.downcast::<HttpError>() {
354 Ok(http_err) => *http_err,
355 Err(err) => {
356 tracing::error!(
363 error = %err,
364 "buffer worker closed unexpectedly; service unavailable"
365 );
366 HttpError::ServiceClosed
367 }
368 }
369}
370
371pub async fn try_acquire_buffer_slot(service: &mut BufferedService) -> Result<(), HttpError> {
376 use std::task::Poll;
377
378 let poll_result = std::future::poll_fn(|cx| match service.poll_ready(cx) {
380 Poll::Ready(result) => Poll::Ready(Some(result)),
381 Poll::Pending => Poll::Ready(None), })
383 .await;
384
385 match poll_result {
386 Some(Ok(())) => Ok(()),
387 Some(Err(e)) => Err(map_buffer_error(e)),
388 None => Err(HttpError::Overloaded), }
390}
391
392#[cfg(test)]
405#[cfg(not(feature = "fips"))]
406#[cfg_attr(coverage_nightly, coverage(off))]
407mod tests {
408 use super::*;
409 use crate::error::HttpError;
410 use httpmock::prelude::*;
411 use serde_json::json;
412
413 fn test_client() -> HttpClient {
414 HttpClientBuilder::new().retry(None).build().unwrap()
415 }
416
417 #[tokio::test]
418 async fn test_http_client_get() {
419 let server = MockServer::start();
420 let _m = server.mock(|when, then| {
421 when.method(Method::GET).path("/test");
422 then.status(200).json_body(json!({"success": true}));
423 });
424
425 let client = test_client();
426 let url = format!("{}/test", server.base_url());
427 let resp = client.get(&url).send().await.unwrap();
428
429 assert_eq!(resp.status(), hyper::StatusCode::OK);
430 }
431
432 #[tokio::test]
433 async fn test_http_client_post() {
434 let server = MockServer::start();
435 let _m = server.mock(|when, then| {
436 when.method(Method::POST).path("/action");
437 then.status(200).json_body(json!({"ok": true}));
438 });
439
440 let client = test_client();
441 let url = format!("{}/action", server.base_url());
442 let resp = client.post(&url).send().await.unwrap();
443
444 assert_eq!(resp.status(), hyper::StatusCode::OK);
445 }
446
447 #[tokio::test]
448 async fn test_http_client_post_form() {
449 let server = MockServer::start();
450 let _m = server.mock(|when, then| {
451 when.method(Method::POST)
452 .path("/submit")
453 .header("content-type", "application/x-www-form-urlencoded")
454 .body("key1=value1&key2=value2");
455 then.status(200).json_body(json!({"received": true}));
456 });
457
458 let client = test_client();
459 let url = format!("{}/submit", server.base_url());
460
461 let resp = client
462 .post(&url)
463 .form(&[("key1", "value1"), ("key2", "value2")])
464 .unwrap()
465 .send()
466 .await
467 .unwrap();
468 assert_eq!(resp.status(), hyper::StatusCode::OK);
469 }
470
471 #[tokio::test]
472 async fn test_json_body_parsing() {
473 #[derive(serde::Deserialize)]
474 struct TestResponse {
475 name: String,
476 value: i32,
477 }
478
479 let server = MockServer::start();
480 let _m = server.mock(|when, then| {
481 when.method(Method::GET).path("/json");
482 then.status(200)
483 .json_body(json!({"name": "test", "value": 42}));
484 });
485
486 let client = test_client();
487 let url = format!("{}/json", server.base_url());
488
489 let data: TestResponse = client.get(&url).send().await.unwrap().json().await.unwrap();
490 assert_eq!(data.name, "test");
491 assert_eq!(data.value, 42);
492 }
493
494 #[tokio::test]
495 async fn test_body_size_limit() {
496 let server = MockServer::start();
497 let large_body = "x".repeat(1024 * 1024); let _m = server.mock(|when, then| {
499 when.method(Method::GET).path("/large");
500 then.status(200).body(&large_body);
501 });
502
503 let client = HttpClientBuilder::new()
504 .retry(None)
505 .max_body_size(1024) .build()
507 .unwrap();
508
509 let url = format!("{}/large", server.base_url());
510 let result = client.get(&url).send().await.unwrap().bytes().await;
511
512 assert!(matches!(result, Err(HttpError::BodyTooLarge { .. })));
513 }
514
515 #[tokio::test]
516 async fn test_custom_user_agent() {
517 let server = MockServer::start();
518 let _m = server.mock(|when, then| {
519 when.method(Method::GET)
520 .path("/test")
521 .header("user-agent", "custom/1.0");
522 then.status(200);
523 });
524
525 let client = HttpClientBuilder::new()
526 .retry(None)
527 .user_agent("custom/1.0")
528 .build()
529 .unwrap();
530
531 let url = format!("{}/test", server.base_url());
532 let resp = client.get(&url).send().await.unwrap();
533 assert_eq!(resp.status(), hyper::StatusCode::OK);
534 }
535
536 #[tokio::test]
537 async fn test_non_2xx_returns_http_status_error() {
538 let server = MockServer::start();
539 let _m = server.mock(|when, then| {
540 when.method(Method::GET).path("/error");
541 then.status(404)
542 .header("content-type", "application/json")
543 .body(r#"{"error": "not found"}"#);
544 });
545
546 let client = test_client();
547 let url = format!("{}/error", server.base_url());
548
549 let result: Result<serde_json::Value, _> =
550 client.get(&url).send().await.unwrap().json().await;
551 match result {
552 Err(HttpError::HttpStatus {
553 status,
554 body_preview,
555 content_type,
556 ..
557 }) => {
558 assert_eq!(status, hyper::StatusCode::NOT_FOUND);
559 assert!(body_preview.contains("not found"));
560 assert_eq!(content_type, Some("application/json".to_owned()));
561 }
562 other => panic!("Expected HttpStatus error, got: {other:?}"),
563 }
564 }
565
566 #[tokio::test]
567 async fn test_checked_body_success() {
568 let server = MockServer::start();
569 let _m = server.mock(|when, then| {
570 when.method(Method::GET).path("/data");
571 then.status(200).body("hello world");
572 });
573
574 let client = test_client();
575 let url = format!("{}/data", server.base_url());
576
577 let body = client
578 .get(&url)
579 .send()
580 .await
581 .unwrap()
582 .checked_bytes()
583 .await
584 .unwrap();
585 assert_eq!(&body[..], b"hello world");
586 }
587
588 #[tokio::test]
589 async fn test_client_is_clone() {
590 let client = test_client();
591 let client2 = client.clone();
592
593 let server = MockServer::start();
595 let _m = server.mock(|when, then| {
596 when.method(Method::GET).path("/test");
597 then.status(200);
598 });
599
600 let url = format!("{}/test", server.base_url());
601 let resp1 = client.get(&url).send().await.unwrap();
602 let resp2 = client2.get(&url).send().await.unwrap();
603
604 assert_eq!(resp1.status(), hyper::StatusCode::OK);
605 assert_eq!(resp2.status(), hyper::StatusCode::OK);
606 }
607
608 #[test]
612 fn test_http_client_is_send_sync() {
613 fn assert_send_sync<T: Send + Sync>() {}
614 assert_send_sync::<HttpClient>();
615 }
616
617 #[tokio::test]
619 async fn test_concurrent_requests_50() {
620 let server = MockServer::start();
621 let _m = server.mock(|when, then| {
622 when.method(Method::GET).path("/concurrent");
623 then.status(200).body("ok");
624 });
625
626 let client = test_client();
627 let url = format!("{}/concurrent", server.base_url());
628
629 let handles: Vec<_> = (0..50)
631 .map(|_| {
632 let client = client.clone();
633 let url = url.clone();
634 tokio::spawn(async move { client.get(&url).send().await })
635 })
636 .collect();
637
638 for handle in handles {
640 let resp = handle.await.unwrap().unwrap();
641 assert_eq!(resp.status(), hyper::StatusCode::OK);
642 }
643 }
644
645 #[tokio::test]
653 async fn test_small_buffer_capacity_no_deadlock() {
654 use crate::config::HttpClientConfig;
655
656 let server = MockServer::start();
657 let _m = server.mock(|when, then| {
658 when.method(Method::GET).path("/test");
659 then.status(200).body("ok");
660 });
661
662 let config = HttpClientConfig {
664 transport: crate::config::TransportSecurity::AllowInsecureHttp,
665 retry: None,
666 rate_limit: None,
667 buffer_capacity: 2,
668 ..Default::default()
669 };
670
671 let client = HttpClientBuilder::with_config(config).build().unwrap();
672 let url = format!("{}/test", server.base_url());
673
674 let handles: Vec<_> = (0..10)
676 .map(|_| {
677 let client = client.clone();
678 let url = url.clone();
679 tokio::spawn(async move { client.get(&url).send().await })
680 })
681 .collect();
682
683 let timeout_result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
685 let mut results = Vec::new();
686 for handle in handles {
687 results.push(handle.await);
688 }
689 results
690 })
691 .await;
692
693 let results = timeout_result.expect("requests should complete within timeout");
694
695 let mut success_count = 0;
696 let mut overloaded_count = 0;
697 for result in results {
698 match result.unwrap() {
699 Ok(resp) => {
700 assert_eq!(resp.status(), hyper::StatusCode::OK);
701 success_count += 1;
702 }
703 Err(HttpError::Overloaded) => {
704 overloaded_count += 1;
705 }
706 Err(e) => panic!("unexpected error: {e:?}"),
707 }
708 }
709
710 assert!(success_count > 0, "at least one request should succeed");
712 assert_eq!(success_count + overloaded_count, 10);
714 }
715
716 #[tokio::test]
721 async fn test_buffer_overflow_returns_overloaded() {
722 use crate::config::HttpClientConfig;
723
724 let server = MockServer::start();
725
726 let _m = server.mock(|when, then| {
727 when.method(Method::GET).path("/slow");
728 then.status(200).body("ok");
729 });
730
731 let config = HttpClientConfig {
733 transport: crate::config::TransportSecurity::AllowInsecureHttp,
734 retry: None,
735 rate_limit: None,
736 buffer_capacity: 1,
737 ..Default::default()
738 };
739
740 let client = HttpClientBuilder::with_config(config).build().unwrap();
741 let url = format!("{}/slow", server.base_url());
742
743 let client1 = client.clone();
745 let url1 = url.clone();
746 let handle1 = tokio::spawn(async move { client1.get(&url1).send().await });
747
748 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
750
751 let result2 = tokio::time::timeout(
753 std::time::Duration::from_millis(50),
754 client.get(&url).send(),
755 )
756 .await;
757
758 let inner_result = result2.expect("request should not timeout waiting for buffer");
760 match inner_result {
761 Err(HttpError::Overloaded) | Ok(_) => {}
763 Err(e) => panic!("unexpected error: {e:?}"),
764 }
765
766 _ = handle1.await;
768 }
769
770 #[tokio::test]
772 async fn test_large_body_no_deadlock() {
773 let server = MockServer::start();
774 let large_body = "x".repeat(100 * 1024); let _m = server.mock(|when, then| {
776 when.method(Method::GET).path("/large");
777 then.status(200).body(&large_body);
778 });
779
780 let client = HttpClientBuilder::new()
781 .retry(None)
782 .max_body_size(1024 * 1024) .build()
784 .unwrap();
785
786 let url = format!("{}/large", server.base_url());
787
788 let handles: Vec<_> = (0..5)
790 .map(|_| {
791 let client = client.clone();
792 let url = url.clone();
793 tokio::spawn(async move { client.get(&url).send().await?.checked_bytes().await })
794 })
795 .collect();
796
797 let timeout_result = tokio::time::timeout(std::time::Duration::from_secs(10), async {
799 let mut results = Vec::new();
800 for handle in handles {
801 results.push(handle.await);
802 }
803 results
804 })
805 .await;
806
807 let results = timeout_result.expect("body reads should complete within timeout");
808 for result in results {
809 let body = result.unwrap().unwrap();
810 assert_eq!(body.len(), 100 * 1024);
811 }
812 }
813
814 #[tokio::test]
820 async fn test_token_endpoint_post_not_retried() {
821 use crate::config::HttpClientConfig;
822
823 let server = MockServer::start();
824
825 let mock = server.mock(|when, then| {
827 when.method(Method::POST).path("/token");
828 then.status(500).body("server error");
829 });
830
831 let mut config = HttpClientConfig::token_endpoint();
833 config.transport = crate::config::TransportSecurity::AllowInsecureHttp; let client = HttpClientBuilder::with_config(config).build().unwrap();
836 let url = format!("{}/token", server.base_url());
837
838 let result = client
840 .post(&url)
841 .form(&[("grant_type", "client_credentials"), ("client_id", "test")])
842 .unwrap()
843 .send()
844 .await;
845
846 assert!(result.is_ok()); let response = result.unwrap();
849 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
850
851 assert_eq!(
854 mock.calls(),
855 1,
856 "POST should not be retried; expected 1 call, got {}",
857 mock.calls()
858 );
859 }
860
861 #[tokio::test]
867 async fn test_http_client_put() {
868 let server = MockServer::start();
869 let _m = server.mock(|when, then| {
870 when.method(Method::PUT).path("/resource");
871 then.status(200).json_body(json!({"updated": true}));
872 });
873
874 let client = test_client();
875 let url = format!("{}/resource", server.base_url());
876 let resp = client.put(&url).send().await.unwrap();
877
878 assert_eq!(resp.status(), hyper::StatusCode::OK);
879 }
880
881 #[tokio::test]
882 async fn test_http_client_put_form() {
883 let server = MockServer::start();
884 let _m = server.mock(|when, then| {
885 when.method(Method::PUT)
886 .path("/resource")
887 .header("content-type", "application/x-www-form-urlencoded")
888 .body("name=updated&value=123");
889 then.status(200).json_body(json!({"updated": true}));
890 });
891
892 let client = test_client();
893 let url = format!("{}/resource", server.base_url());
894
895 let resp = client
896 .put(&url)
897 .form(&[("name", "updated"), ("value", "123")])
898 .unwrap()
899 .send()
900 .await
901 .unwrap();
902 assert_eq!(resp.status(), hyper::StatusCode::OK);
903 }
904
905 #[tokio::test]
906 async fn test_http_client_patch() {
907 let server = MockServer::start();
908 let _m = server.mock(|when, then| {
909 when.method(Method::PATCH).path("/resource/1");
910 then.status(200).json_body(json!({"patched": true}));
911 });
912
913 let client = test_client();
914 let url = format!("{}/resource/1", server.base_url());
915 let resp = client.patch(&url).send().await.unwrap();
916
917 assert_eq!(resp.status(), hyper::StatusCode::OK);
918 }
919
920 #[tokio::test]
921 async fn test_http_client_patch_form() {
922 let server = MockServer::start();
923 let _m = server.mock(|when, then| {
924 when.method(Method::PATCH)
925 .path("/resource/1")
926 .header("content-type", "application/x-www-form-urlencoded")
927 .body("field=patched");
928 then.status(200).json_body(json!({"patched": true}));
929 });
930
931 let client = test_client();
932 let url = format!("{}/resource/1", server.base_url());
933
934 let resp = client
935 .patch(&url)
936 .form(&[("field", "patched")])
937 .unwrap()
938 .send()
939 .await
940 .unwrap();
941 assert_eq!(resp.status(), hyper::StatusCode::OK);
942 }
943
944 #[tokio::test]
945 async fn test_http_client_delete() {
946 let server = MockServer::start();
947 let _m = server.mock(|when, then| {
948 when.method(Method::DELETE).path("/resource/42");
949 then.status(204);
950 });
951
952 let client = test_client();
953 let url = format!("{}/resource/42", server.base_url());
954 let resp = client.delete(&url).send().await.unwrap();
955
956 assert_eq!(resp.status(), hyper::StatusCode::NO_CONTENT);
957 }
958
959 #[tokio::test]
960 async fn test_http_client_delete_returns_200() {
961 let server = MockServer::start();
962 let _m = server.mock(|when, then| {
963 when.method(Method::DELETE).path("/resource/99");
964 then.status(200).json_body(json!({"deleted": true}));
965 });
966
967 let client = test_client();
968 let url = format!("{}/resource/99", server.base_url());
969 let resp = client.delete(&url).send().await.unwrap();
970
971 assert_eq!(resp.status(), hyper::StatusCode::OK);
972 }
973
974 #[tokio::test]
975 async fn test_http_client_head_returns_headers_no_body() {
976 let server = MockServer::start();
977 let _m = server.mock(|when, then| {
978 when.method(Method::HEAD).path("/probe");
979 then.status(200)
980 .header("content-length", "12345")
981 .header("etag", "\"abc\"");
982 });
983
984 let client = test_client();
985 let url = format!("{}/probe", server.base_url());
986 let resp = client.head(&url).send().await.unwrap();
987
988 assert_eq!(resp.status(), hyper::StatusCode::OK);
989 assert_eq!(
990 resp.headers()
991 .get("content-length")
992 .and_then(|v| v.to_str().ok()),
993 Some("12345"),
994 );
995 let body = resp.bytes().await.unwrap();
996 assert!(body.is_empty(), "HEAD response body must be empty");
997 }
998
999 #[tokio::test]
1000 async fn test_http_client_options_returns_allow_header() {
1001 let server = MockServer::start();
1002 let _m = server.mock(|when, then| {
1003 when.method(Method::OPTIONS).path("/resource");
1004 then.status(204).header("allow", "GET, POST, PUT, DELETE");
1005 });
1006
1007 let client = test_client();
1008 let url = format!("{}/resource", server.base_url());
1009 let resp = client.options(&url).send().await.unwrap();
1010
1011 assert_eq!(resp.status(), hyper::StatusCode::NO_CONTENT);
1012 assert_eq!(
1013 resp.headers().get("allow").and_then(|v| v.to_str().ok()),
1014 Some("GET, POST, PUT, DELETE"),
1015 );
1016 }
1017
1018 #[tokio::test]
1019 async fn test_put_form_with_custom_headers() {
1020 let server = MockServer::start();
1021 let _m = server.mock(|when, then| {
1022 when.method(Method::PUT)
1023 .path("/api/data")
1024 .header("content-type", "application/x-www-form-urlencoded")
1025 .header("x-custom-header", "custom-value")
1026 .body("key=value");
1027 then.status(200);
1028 });
1029
1030 let client = test_client();
1031 let url = format!("{}/api/data", server.base_url());
1032
1033 let resp = client
1034 .put(&url)
1035 .header("x-custom-header", "custom-value")
1036 .form(&[("key", "value")])
1037 .unwrap()
1038 .send()
1039 .await
1040 .unwrap();
1041 assert_eq!(resp.status(), hyper::StatusCode::OK);
1042 }
1043
1044 #[tokio::test]
1045 async fn test_patch_form_with_custom_headers() {
1046 let server = MockServer::start();
1047 let _m = server.mock(|when, then| {
1048 when.method(Method::PATCH)
1049 .path("/api/data")
1050 .header("content-type", "application/x-www-form-urlencoded")
1051 .header("authorization", "Bearer token123")
1052 .body("status=active");
1053 then.status(200);
1054 });
1055
1056 let client = test_client();
1057 let url = format!("{}/api/data", server.base_url());
1058
1059 let resp = client
1060 .patch(&url)
1061 .header("authorization", "Bearer token123")
1062 .form(&[("status", "active")])
1063 .unwrap()
1064 .send()
1065 .await
1066 .unwrap();
1067 assert_eq!(resp.status(), hyper::StatusCode::OK);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_request_builder_json_body() {
1072 #[derive(serde::Serialize)]
1073 struct CreateUser {
1074 name: String,
1075 email: String,
1076 }
1077
1078 let server = MockServer::start();
1079 let _m = server.mock(|when, then| {
1080 when.method(Method::POST)
1081 .path("/users")
1082 .header("content-type", "application/json")
1083 .json_body(json!({"name": "Alice", "email": "alice@example.com"}));
1084 then.status(201).json_body(json!({"id": 1}));
1085 });
1086
1087 let client = test_client();
1088 let url = format!("{}/users", server.base_url());
1089
1090 let resp = client
1091 .post(&url)
1092 .json(&CreateUser {
1093 name: "Alice".into(),
1094 email: "alice@example.com".into(),
1095 })
1096 .unwrap()
1097 .send()
1098 .await
1099 .unwrap();
1100 assert_eq!(resp.status(), hyper::StatusCode::CREATED);
1101 }
1102
1103 #[tokio::test]
1104 async fn test_request_builder_body_bytes() {
1105 let server = MockServer::start();
1106 let _m = server.mock(|when, then| {
1107 when.method(Method::POST)
1108 .path("/upload")
1109 .body("raw binary data");
1110 then.status(200);
1111 });
1112
1113 let client = test_client();
1114 let url = format!("{}/upload", server.base_url());
1115
1116 let resp = client
1117 .post(&url)
1118 .body_bytes(bytes::Bytes::from("raw binary data"))
1119 .send()
1120 .await
1121 .unwrap();
1122 assert_eq!(resp.status(), hyper::StatusCode::OK);
1123 }
1124
1125 #[tokio::test]
1131 async fn test_content_type_not_duplicated_with_json() {
1132 #[derive(serde::Serialize)]
1133 struct TestData {
1134 value: i32,
1135 }
1136
1137 let server = MockServer::start();
1138 let mock = server.mock(|when, then| {
1139 when.method(Method::POST)
1140 .path("/custom-content-type")
1141 .header("content-type", "application/vnd.custom+json");
1143 then.status(200);
1144 });
1145
1146 let client = test_client();
1147 let url = format!("{}/custom-content-type", server.base_url());
1148
1149 let resp = client
1150 .post(&url)
1151 .header("content-type", "application/vnd.custom+json") .json(&TestData { value: 42 })
1153 .unwrap()
1154 .send()
1155 .await
1156 .unwrap();
1157
1158 assert_eq!(resp.status(), hyper::StatusCode::OK);
1159 assert_eq!(
1160 mock.calls(),
1161 1,
1162 "Request with custom Content-Type should match"
1163 );
1164 }
1165
1166 #[tokio::test]
1168 async fn test_content_type_not_duplicated_with_form() {
1169 let server = MockServer::start();
1170 let mock = server.mock(|when, then| {
1171 when.method(Method::POST)
1172 .path("/custom-form-type")
1173 .header("content-type", "application/x-custom-form");
1175 then.status(200);
1176 });
1177
1178 let client = test_client();
1179 let url = format!("{}/custom-form-type", server.base_url());
1180
1181 let resp = client
1182 .post(&url)
1183 .header("content-type", "application/x-custom-form") .form(&[("key", "value")])
1185 .unwrap()
1186 .send()
1187 .await
1188 .unwrap();
1189
1190 assert_eq!(resp.status(), hyper::StatusCode::OK);
1191 assert_eq!(
1192 mock.calls(),
1193 1,
1194 "Request with custom Content-Type should match"
1195 );
1196 }
1197
1198 #[tokio::test]
1199 async fn test_request_builder_body_string() {
1200 let server = MockServer::start();
1201 let _m = server.mock(|when, then| {
1202 when.method(Method::POST)
1203 .path("/text")
1204 .body("Hello, World!");
1205 then.status(200);
1206 });
1207
1208 let client = test_client();
1209 let url = format!("{}/text", server.base_url());
1210
1211 let resp = client
1212 .post(&url)
1213 .body_string("Hello, World!".into())
1214 .send()
1215 .await
1216 .unwrap();
1217 assert_eq!(resp.status(), hyper::StatusCode::OK);
1218 }
1219
1220 #[tokio::test]
1221 async fn test_response_text_method() {
1222 let server = MockServer::start();
1223 let _m = server.mock(|when, then| {
1224 when.method(Method::GET).path("/text");
1225 then.status(200).body("Hello, World!");
1226 });
1227
1228 let client = test_client();
1229 let url = format!("{}/text", server.base_url());
1230
1231 let text = client.get(&url).send().await.unwrap().text().await.unwrap();
1232 assert_eq!(text, "Hello, World!");
1233 }
1234
1235 #[tokio::test]
1236 async fn test_request_builder_multiple_headers() {
1237 let server = MockServer::start();
1238 let _m = server.mock(|when, then| {
1239 when.method(Method::GET)
1240 .path("/headers")
1241 .header("x-first", "one")
1242 .header("x-second", "two");
1243 then.status(200);
1244 });
1245
1246 let client = test_client();
1247 let url = format!("{}/headers", server.base_url());
1248
1249 let resp = client
1250 .get(&url)
1251 .header("x-first", "one")
1252 .header("x-second", "two")
1253 .send()
1254 .await
1255 .unwrap();
1256 assert_eq!(resp.status(), hyper::StatusCode::OK);
1257 }
1258
1259 #[tokio::test]
1260 async fn test_request_builder_headers_vec() {
1261 let server = MockServer::start();
1262 let _m = server.mock(|when, then| {
1263 when.method(Method::GET)
1264 .path("/headers")
1265 .header("x-first", "one")
1266 .header("x-second", "two");
1267 then.status(200);
1268 });
1269
1270 let client = test_client();
1271 let url = format!("{}/headers", server.base_url());
1272
1273 let resp = client
1274 .get(&url)
1275 .headers(vec![
1276 ("x-first".to_owned(), "one".to_owned()),
1277 ("x-second".to_owned(), "two".to_owned()),
1278 ])
1279 .send()
1280 .await
1281 .unwrap();
1282 assert_eq!(resp.status(), hyper::StatusCode::OK);
1283 }
1284
1285 #[tokio::test]
1288 async fn test_error_response_with_large_body_returns_http_status() {
1289 use crate::security::ERROR_BODY_PREVIEW_LIMIT;
1290
1291 let server = MockServer::start();
1292
1293 let large_body = "x".repeat(ERROR_BODY_PREVIEW_LIMIT + 1000);
1295
1296 let _m = server.mock(|when, then| {
1297 when.method(Method::GET).path("/error-with-large-body");
1298 then.status(500).body(&large_body);
1299 });
1300
1301 let client = test_client();
1302 let url = format!("{}/error-with-large-body", server.base_url());
1303
1304 let result = client.get(&url).send().await.unwrap().checked_bytes().await;
1305
1306 match result {
1308 Err(HttpError::HttpStatus {
1309 status,
1310 body_preview,
1311 ..
1312 }) => {
1313 assert_eq!(status, hyper::StatusCode::INTERNAL_SERVER_ERROR);
1314 assert_eq!(body_preview, "<body too large for preview>");
1316 }
1317 Err(HttpError::BodyTooLarge { .. }) => {
1318 panic!("Should return HttpStatus, not BodyTooLarge for non-2xx responses");
1319 }
1320 Err(other) => panic!("Unexpected error: {other:?}"),
1321 Ok(_) => panic!("Should have returned an error for 500 status"),
1322 }
1323 }
1324
1325 fn gzip_compress(data: &[u8]) -> Vec<u8> {
1331 use flate2::Compression;
1332 use flate2::write::GzEncoder;
1333 use std::io::Write;
1334
1335 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1336 encoder.write_all(data).unwrap();
1337 encoder.finish().unwrap()
1338 }
1339
1340 #[tokio::test]
1345 async fn test_gzip_decompression_basic() {
1346 let server = MockServer::start();
1347
1348 let original_body = b"Hello, this is a test body that will be gzip compressed!";
1349 let compressed_body = gzip_compress(original_body);
1350
1351 let _m = server.mock(|when, then| {
1352 when.method(Method::GET).path("/gzip");
1353 then.status(200)
1354 .header("content-encoding", "gzip")
1355 .body(compressed_body);
1356 });
1357
1358 let client = test_client();
1359 let url = format!("{}/gzip", server.base_url());
1360
1361 let body = client
1362 .get(&url)
1363 .send()
1364 .await
1365 .unwrap()
1366 .bytes()
1367 .await
1368 .unwrap();
1369
1370 assert_eq!(
1371 body.as_ref(),
1372 original_body,
1373 "Decompressed body should match original"
1374 );
1375 }
1376
1377 #[tokio::test]
1382 async fn test_gzip_decompression_json() {
1383 #[derive(serde::Deserialize, PartialEq, Debug)]
1384 struct TestData {
1385 name: String,
1386 value: i32,
1387 nested: NestedData,
1388 }
1389
1390 #[derive(serde::Deserialize, PartialEq, Debug)]
1391 struct NestedData {
1392 items: Vec<String>,
1393 }
1394
1395 let server = MockServer::start();
1396
1397 let json_body = r#"{"name":"test","value":42,"nested":{"items":["a","b","c"]}}"#;
1398 let compressed_body = gzip_compress(json_body.as_bytes());
1399
1400 let _m = server.mock(|when, then| {
1401 when.method(Method::GET).path("/gzip-json");
1402 then.status(200)
1403 .header("content-type", "application/json")
1404 .header("content-encoding", "gzip")
1405 .body(compressed_body);
1406 });
1407
1408 let client = test_client();
1409 let url = format!("{}/gzip-json", server.base_url());
1410
1411 let data: TestData = client.get(&url).send().await.unwrap().json().await.unwrap();
1412
1413 assert_eq!(data.name, "test");
1414 assert_eq!(data.value, 42);
1415 assert_eq!(data.nested.items, vec!["a", "b", "c"]);
1416 }
1417
1418 #[tokio::test]
1426 async fn test_gzip_decompression_body_size_limit() {
1427 let server = MockServer::start();
1428
1429 let large_decompressed = vec![b'x'; 100 * 1024]; let compressed_body = gzip_compress(&large_decompressed);
1433
1434 assert!(
1436 compressed_body.len() < 2000,
1437 "Compressed body should be small (got {} bytes)",
1438 compressed_body.len()
1439 );
1440
1441 let _m = server.mock(|when, then| {
1442 when.method(Method::GET).path("/gzip-bomb");
1443 then.status(200)
1444 .header("content-encoding", "gzip")
1445 .body(compressed_body);
1446 });
1447
1448 let client = HttpClientBuilder::new()
1450 .retry(None)
1451 .max_body_size(10 * 1024) .build()
1453 .unwrap();
1454
1455 let url = format!("{}/gzip-bomb", server.base_url());
1456 let result = client.get(&url).send().await.unwrap().bytes().await;
1457
1458 match result {
1460 Err(HttpError::BodyTooLarge { limit, actual }) => {
1461 assert_eq!(limit, 10 * 1024, "Limit should be 10KB");
1462 assert!(
1463 actual > limit,
1464 "Actual size ({actual}) should exceed limit ({limit})"
1465 );
1466 }
1467 Err(other) => panic!("Expected BodyTooLarge error, got: {other:?}"),
1468 Ok(body) => panic!(
1469 "Expected BodyTooLarge error, but got {} bytes of body",
1470 body.len()
1471 ),
1472 }
1473 }
1474
1475 #[tokio::test]
1480 async fn test_accept_encoding_header_sent() {
1481 let server = MockServer::start();
1482
1483 let mock = server.mock(|when, then| {
1485 when.method(Method::GET)
1486 .path("/check-accept-encoding")
1487 .header_exists("accept-encoding");
1488 then.status(200).body("ok");
1489 });
1490
1491 let client = test_client();
1492 let url = format!("{}/check-accept-encoding", server.base_url());
1493
1494 let resp = client.get(&url).send().await.unwrap();
1495 assert_eq!(resp.status(), hyper::StatusCode::OK);
1496
1497 assert_eq!(
1499 mock.calls(),
1500 1,
1501 "Request should have included Accept-Encoding header"
1502 );
1503 }
1504
1505 #[tokio::test]
1509 async fn test_no_compression_passthrough() {
1510 let server = MockServer::start();
1511
1512 let plain_body = b"This is plain text, not compressed";
1513
1514 let _m = server.mock(|when, then| {
1515 when.method(Method::GET).path("/plain");
1516 then.status(200)
1517 .header("content-type", "text/plain")
1518 .body(plain_body.as_slice());
1519 });
1520
1521 let client = test_client();
1522 let url = format!("{}/plain", server.base_url());
1523
1524 let body = client
1525 .get(&url)
1526 .send()
1527 .await
1528 .unwrap()
1529 .bytes()
1530 .await
1531 .unwrap();
1532
1533 assert_eq!(
1534 body.as_ref(),
1535 plain_body,
1536 "Plain body should pass through unchanged"
1537 );
1538 }
1539
1540 #[tokio::test]
1542 async fn test_gzip_decompression_checked_bytes() {
1543 let server = MockServer::start();
1544
1545 let original_body = b"Checked bytes test with gzip";
1546 let compressed_body = gzip_compress(original_body);
1547
1548 let _m = server.mock(|when, then| {
1549 when.method(Method::GET).path("/gzip-checked");
1550 then.status(200)
1551 .header("content-encoding", "gzip")
1552 .body(compressed_body);
1553 });
1554
1555 let client = test_client();
1556 let url = format!("{}/gzip-checked", server.base_url());
1557
1558 let body = client
1559 .get(&url)
1560 .send()
1561 .await
1562 .unwrap()
1563 .checked_bytes()
1564 .await
1565 .unwrap();
1566
1567 assert_eq!(
1568 body.as_ref(),
1569 original_body,
1570 "checked_bytes should return decompressed content"
1571 );
1572 }
1573
1574 #[tokio::test]
1576 async fn test_gzip_decompression_text() {
1577 let server = MockServer::start();
1578
1579 let original_text = "Hello, World! \u{1F600}"; let compressed_body = gzip_compress(original_text.as_bytes());
1581
1582 let _m = server.mock(|when, then| {
1583 when.method(Method::GET).path("/gzip-text");
1584 then.status(200)
1585 .header("content-type", "text/plain; charset=utf-8")
1586 .header("content-encoding", "gzip")
1587 .body(compressed_body);
1588 });
1589
1590 let client = test_client();
1591 let url = format!("{}/gzip-text", server.base_url());
1592
1593 let text = client.get(&url).send().await.unwrap().text().await.unwrap();
1594
1595 assert_eq!(
1596 text, original_text,
1597 "text() should return decompressed UTF-8 content"
1598 );
1599 }
1600
1601 #[test]
1607 fn test_map_buffer_error_passes_through_http_error() {
1608 let http_err = HttpError::Timeout(std::time::Duration::from_secs(10));
1609 let boxed: tower::BoxError = Box::new(http_err);
1610 let result = map_buffer_error(boxed);
1611
1612 assert!(
1613 matches!(result, HttpError::Timeout(_)),
1614 "Should pass through HttpError::Timeout, got: {result:?}"
1615 );
1616 }
1617
1618 #[test]
1623 fn test_map_buffer_error_returns_service_closed_for_unknown_error() {
1624 let other_err: tower::BoxError = Box::new(std::io::Error::new(
1626 std::io::ErrorKind::BrokenPipe,
1627 "buffer worker died",
1628 ));
1629 let result = map_buffer_error(other_err);
1630
1631 assert!(
1632 matches!(result, HttpError::ServiceClosed),
1633 "Should return ServiceClosed for non-HttpError, got: {result:?}"
1634 );
1635 }
1636
1637 #[tokio::test]
1654 async fn test_status_retry_get_500_retried() {
1655 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1656
1657 let server = MockServer::start();
1658 let mock = server.mock(|when, then| {
1659 when.method(Method::GET).path("/retry-500");
1660 then.status(500).body("server error");
1661 });
1662
1663 let config = HttpClientConfig {
1664 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1665 retry: Some(RetryConfig {
1666 max_retries: 2, backoff: ExponentialBackoff::fast(),
1668 ..RetryConfig::default() }),
1670 rate_limit: None,
1671 ..Default::default()
1672 };
1673
1674 let client = HttpClientBuilder::with_config(config).build().unwrap();
1675 let url = format!("{}/retry-500", server.base_url());
1676
1677 let result = client.get(&url).send().await;
1678
1679 assert_eq!(
1681 mock.calls(),
1682 3,
1683 "GET should retry on 500; expected 3 calls (1 + 2 retries), got {}",
1684 mock.calls()
1685 );
1686
1687 let response = result.expect("send() should return Ok(Response) after retries exhaust");
1689 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
1690
1691 let err = response.error_for_status().unwrap_err();
1693 assert!(
1694 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
1695 );
1696 }
1697
1698 #[tokio::test]
1707 async fn test_status_retry_post_500_not_retried() {
1708 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1709
1710 let server = MockServer::start();
1711 let mock = server.mock(|when, then| {
1712 when.method(Method::POST).path("/post-500");
1713 then.status(500).body("server error");
1714 });
1715
1716 let config = HttpClientConfig {
1717 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1718 retry: Some(RetryConfig {
1719 max_retries: 3,
1720 backoff: ExponentialBackoff::fast(),
1721 ..RetryConfig::default() }),
1723 rate_limit: None,
1724 ..Default::default()
1725 };
1726
1727 let client = HttpClientBuilder::with_config(config).build().unwrap();
1728 let url = format!("{}/post-500", server.base_url());
1729
1730 let result = client.post(&url).send().await;
1731
1732 assert_eq!(
1734 mock.calls(),
1735 1,
1736 "POST should not be retried on 500; expected 1 call, got {}",
1737 mock.calls()
1738 );
1739
1740 let response = result.expect("POST + 500 should return Ok(Response), not Err");
1743 assert_eq!(
1744 response.status(),
1745 hyper::StatusCode::INTERNAL_SERVER_ERROR,
1746 "Response should have 500 status"
1747 );
1748
1749 }
1751
1752 #[tokio::test]
1758 async fn test_status_retry_post_429_retried() {
1759 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1760
1761 let server = MockServer::start();
1762 let mock = server.mock(|when, then| {
1763 when.method(Method::POST).path("/post-429");
1764 then.status(429).body("rate limited");
1765 });
1766
1767 let config = HttpClientConfig {
1768 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1769 retry: Some(RetryConfig {
1770 max_retries: 2, backoff: ExponentialBackoff::fast(),
1772 ..RetryConfig::default() }),
1774 rate_limit: None,
1775 ..Default::default()
1776 };
1777
1778 let client = HttpClientBuilder::with_config(config).build().unwrap();
1779 let url = format!("{}/post-429", server.base_url());
1780
1781 let result = client.post(&url).send().await;
1782
1783 assert_eq!(
1785 mock.calls(),
1786 3,
1787 "POST should retry on 429; expected 3 calls (1 + 2 retries), got {}",
1788 mock.calls()
1789 );
1790
1791 let response = result.expect("send() should return Ok(Response) after retries exhaust");
1793 assert_eq!(response.status(), hyper::StatusCode::TOO_MANY_REQUESTS);
1794
1795 let err = response.error_for_status().unwrap_err();
1797 assert!(
1798 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::TOO_MANY_REQUESTS)
1799 );
1800 }
1801
1802 #[tokio::test]
1807 async fn test_status_retry_extracts_retry_after_header() {
1808 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1809
1810 let server = MockServer::start();
1811 let _mock = server.mock(|when, then| {
1812 when.method(Method::GET).path("/retry-after");
1813 then.status(429)
1814 .header("Retry-After", "60")
1815 .header("Content-Type", "application/json")
1816 .body(r#"{"error": "rate limited"}"#);
1817 });
1818
1819 let config = HttpClientConfig {
1820 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1821 retry: Some(RetryConfig {
1822 max_retries: 0, backoff: ExponentialBackoff::fast(),
1824 ..RetryConfig::default()
1825 }),
1826 rate_limit: None,
1827 ..Default::default()
1828 };
1829
1830 let client = HttpClientBuilder::with_config(config).build().unwrap();
1831 let url = format!("{}/retry-after", server.base_url());
1832
1833 let result = client.get(&url).send().await;
1834
1835 let response = result.expect("send() should return Ok(Response)");
1837 assert_eq!(response.status(), hyper::StatusCode::TOO_MANY_REQUESTS);
1838
1839 match response.error_for_status() {
1841 Err(HttpError::HttpStatus {
1842 status,
1843 retry_after,
1844 content_type,
1845 ..
1846 }) => {
1847 assert_eq!(status, hyper::StatusCode::TOO_MANY_REQUESTS);
1848 assert_eq!(
1849 retry_after,
1850 Some(std::time::Duration::from_mins(1)),
1851 "Should extract Retry-After header"
1852 );
1853 assert_eq!(
1854 content_type,
1855 Some("application/json".to_owned()),
1856 "Should extract Content-Type header"
1857 );
1858 }
1859 other => panic!("Expected HttpStatus error from error_for_status(), got: {other:?}"),
1860 }
1861 }
1862
1863 #[tokio::test]
1875 async fn test_status_retry_ignores_retry_after_when_configured() {
1876 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1877
1878 let server = MockServer::start();
1879 let mock = server.mock(|when, then| {
1880 when.method(Method::GET).path("/ignore-retry-after");
1881 then.status(429)
1882 .header("Retry-After", "10") .body("rate limited");
1884 });
1885
1886 let config = HttpClientConfig {
1887 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1888 retry: Some(RetryConfig {
1889 max_retries: 2,
1890 backoff: ExponentialBackoff::fast(), ignore_retry_after: true, ..RetryConfig::default()
1893 }),
1894 rate_limit: None,
1895 ..Default::default()
1896 };
1897
1898 let client = HttpClientBuilder::with_config(config).build().unwrap();
1899 let url = format!("{}/ignore-retry-after", server.base_url());
1900
1901 let start = std::time::Instant::now();
1902 let _result = client.get(&url).send().await;
1903 let elapsed = start.elapsed();
1904
1905 assert!(
1908 elapsed < std::time::Duration::from_secs(2),
1909 "Should have used fast backoff, not 10s Retry-After; elapsed: {elapsed:?}"
1910 );
1911
1912 assert_eq!(mock.calls(), 3, "Expected 3 calls, got {}", mock.calls());
1914 }
1915
1916 #[tokio::test]
1921 async fn test_non_retryable_status_passes_through() {
1922 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1923
1924 let server = MockServer::start();
1925 let mock = server.mock(|when, then| {
1926 when.method(Method::GET).path("/not-found");
1927 then.status(404)
1928 .header("content-type", "application/json")
1929 .body(r#"{"error": "not found"}"#);
1930 });
1931
1932 let config = HttpClientConfig {
1933 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1934 retry: Some(RetryConfig {
1935 max_retries: 3,
1936 backoff: ExponentialBackoff::fast(),
1937 ..RetryConfig::default()
1938 }),
1939 rate_limit: None,
1940 ..Default::default()
1941 };
1942
1943 let client = HttpClientBuilder::with_config(config).build().unwrap();
1944 let url = format!("{}/not-found", server.base_url());
1945
1946 let result = client.get(&url).send().await;
1948
1949 assert_eq!(
1951 mock.calls(),
1952 1,
1953 "404 should not trigger retry; expected 1 call, got {}",
1954 mock.calls()
1955 );
1956
1957 let response = result.expect("send() should succeed for 404");
1959 assert_eq!(response.status(), hyper::StatusCode::NOT_FOUND);
1960
1961 }
1963
1964 #[tokio::test]
1969 async fn test_status_retry_exhausted_returns_ok_response() {
1970 use crate::config::{ExponentialBackoff, HttpClientConfig, RetryConfig};
1971
1972 let server = MockServer::start();
1973 let mock = server.mock(|when, then| {
1974 when.method(Method::GET).path("/always-500");
1975 then.status(500).body("always fails");
1976 });
1977
1978 let config = HttpClientConfig {
1979 transport: crate::config::TransportSecurity::AllowInsecureHttp,
1980 retry: Some(RetryConfig {
1981 max_retries: 2, backoff: ExponentialBackoff::fast(),
1983 ..RetryConfig::default()
1984 }),
1985 rate_limit: None,
1986 ..Default::default()
1987 };
1988
1989 let client = HttpClientBuilder::with_config(config).build().unwrap();
1990 let url = format!("{}/always-500", server.base_url());
1991
1992 let result = client.get(&url).send().await;
1993
1994 assert_eq!(
1996 mock.calls(),
1997 3,
1998 "Expected 3 calls (1 initial + 2 retries), got {}",
1999 mock.calls()
2000 );
2001
2002 let response = result.expect("send() should return Ok(Response) after retries exhaust");
2004 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
2005
2006 let err = response.error_for_status().unwrap_err();
2008 assert!(
2009 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
2010 );
2011 }
2012
2013 #[tokio::test]
2018 async fn test_no_retry_config_status_passes_through() {
2019 use crate::config::HttpClientConfig;
2020
2021 let server = MockServer::start();
2022 let mock = server.mock(|when, then| {
2023 when.method(Method::GET).path("/no-retry");
2024 then.status(500).body("server error");
2025 });
2026
2027 let config = HttpClientConfig {
2028 transport: crate::config::TransportSecurity::AllowInsecureHttp,
2029 retry: None, rate_limit: None,
2031 ..Default::default()
2032 };
2033
2034 let client = HttpClientBuilder::with_config(config).build().unwrap();
2035 let url = format!("{}/no-retry", server.base_url());
2036
2037 let result = client.get(&url).send().await;
2038
2039 assert_eq!(mock.calls(), 1);
2041
2042 let response = result.expect("send() should succeed when retry disabled");
2044 assert_eq!(response.status(), hyper::StatusCode::INTERNAL_SERVER_ERROR);
2045
2046 let err = response.error_for_status().unwrap_err();
2048 assert!(
2049 matches!(err, HttpError::HttpStatus { status, .. } if status == hyper::StatusCode::INTERNAL_SERVER_ERROR)
2050 );
2051 }
2052
2053 #[tokio::test]
2059 async fn test_url_scheme_http_rejected_with_tls_only() {
2060 let client = HttpClientBuilder::new()
2061 .transport(crate::config::TransportSecurity::TlsOnly)
2062 .retry(None)
2063 .build()
2064 .unwrap();
2065
2066 let result = client.get("http://example.com/test").send().await;
2068
2069 match result {
2071 Err(HttpError::InvalidScheme { scheme, reason }) => {
2072 assert_eq!(scheme, "http");
2073 assert!(
2074 reason.contains("TlsOnly"),
2075 "Error should mention TlsOnly: {reason}"
2076 );
2077 }
2078 Err(other) => panic!("Expected InvalidScheme error, got: {other:?}"),
2079 Ok(_) => panic!("Expected InvalidScheme error, but request succeeded"),
2080 }
2081 }
2082
2083 #[tokio::test]
2085 async fn test_url_scheme_http_allowed_with_allow_insecure() {
2086 let server = MockServer::start();
2087 let _m = server.mock(|when, then| {
2088 when.method(Method::GET).path("/test");
2089 then.status(200).body("ok");
2090 });
2091
2092 let client = HttpClientBuilder::new()
2093 .transport(crate::config::TransportSecurity::AllowInsecureHttp)
2094 .retry(None)
2095 .build()
2096 .unwrap();
2097
2098 let url = format!("{}/test", server.base_url()); let result = client.get(&url).send().await;
2100
2101 assert!(result.is_ok(), "http:// should be allowed: {result:?}");
2102 }
2103
2104 #[tokio::test]
2106 async fn test_url_scheme_https_always_allowed() {
2107 let client = HttpClientBuilder::new()
2110 .transport(crate::config::TransportSecurity::TlsOnly)
2111 .retry(None)
2112 .build()
2113 .unwrap();
2114
2115 let result = client.get("https://localhost:0/test").send().await;
2118
2119 if let Err(HttpError::InvalidScheme { .. }) = result {
2121 panic!("https:// should not trigger InvalidScheme error")
2122 }
2123 }
2125
2126 #[tokio::test]
2132 async fn test_url_scheme_invalid_rejected() {
2133 let client = HttpClientBuilder::new().retry(None).build().unwrap();
2134
2135 let result = client.get("ftp://files.example.com/file.txt").send().await;
2136
2137 match result {
2138 Err(HttpError::InvalidScheme { scheme, reason }) => {
2139 assert_eq!(scheme, "ftp");
2140 assert!(
2141 reason.contains("http://") || reason.contains("https://"),
2142 "Error should mention supported schemes: {reason}"
2143 );
2144 }
2145 Err(other) => panic!("Expected InvalidScheme error, got: {other:?}"),
2146 Ok(_) => panic!("Expected InvalidScheme error, but request succeeded"),
2147 }
2148 }
2149
2150 #[tokio::test]
2156 async fn test_url_scheme_missing_rejected() {
2157 let client = HttpClientBuilder::new().retry(None).build().unwrap();
2158
2159 let result = client.get("example.com/test").send().await;
2160
2161 match result {
2162 Err(HttpError::InvalidUri { url, reason, kind }) => {
2163 assert_eq!(url, "example.com/test");
2165 assert!(!reason.is_empty(), "Should have a reason for invalid URI");
2166 assert_eq!(kind, crate::error::InvalidUriKind::ParseError);
2167 }
2168 Err(other) => panic!("Expected InvalidUri error, got: {other:?}"),
2169 Ok(_) => panic!("Expected InvalidUri error, but request succeeded"),
2170 }
2171 }
2172}