1use std::fmt;
2use std::io::{self, Read};
3use std::sync::atomic::AtomicBool;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9
10use crate::cancel::{is_cancelled_io, is_set, sleep_cancellable, CancelToken, CheckedReader};
11use crate::error::{HttpError, Phase, MAX_ERROR_BODY_BYTES};
12use crate::netpolicy::{denial_in_chain, normalize_host, AddressPolicy, VettingResolver, ViaProxy};
13use crate::redirect;
14use crate::retry::{parse_retry_after, AttemptBudgets, RetryPolicy};
15use crate::sse::SseReader;
16use crate::urlredact::{redact_url, redact_urls_in_text, url_secret_parts};
17
18pub(crate) const REDACTED: &str = "[redacted]";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Method {
22 Get,
23 Post,
24 Put,
25 Patch,
26 Delete,
27 Head,
28}
29
30impl Method {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Method::Get => "GET",
34 Method::Post => "POST",
35 Method::Put => "PUT",
36 Method::Patch => "PATCH",
37 Method::Delete => "DELETE",
38 Method::Head => "HEAD",
39 }
40 }
41 pub fn is_idempotent(self) -> bool {
43 !matches!(self, Method::Post | Method::Patch)
44 }
45}
46
47#[derive(Debug, Clone)]
50pub struct RetryEvent {
51 pub url: String,
52 pub retry: u32,
54 pub reason: String,
55 pub delay: Duration,
56}
57
58type RetryObserver = Arc<dyn Fn(&RetryEvent) + Send + Sync>;
59
60#[derive(Debug, Clone)]
63pub struct AttemptEvent {
64 pub url: String,
65 pub method: Method,
66 pub attempt: u32,
68 pub max_attempts: u32,
70}
71
72type AttemptHook = Arc<dyn Fn(&AttemptEvent) -> Result<(), String> + Send + Sync>;
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum AttemptDelivery {
79 Responded,
81 NotSent,
84 Uncertain,
88}
89
90#[derive(Debug, Clone)]
105pub struct AttemptOutcome {
106 pub url: String,
107 pub method: Method,
108 pub attempt: u32,
110 pub redirect_hop: u32,
112 pub status: Option<u16>,
114 pub headers: Vec<(String, String)>,
116 pub error_class: Option<crate::error::ErrorClass>,
118 pub error_phase: Option<Phase>,
120 pub error: Option<String>,
122 pub delivery: AttemptDelivery,
123 pub next_delay: Option<Duration>,
127}
128
129pub(crate) type AttemptOutcomeObserver = Arc<dyn Fn(&AttemptOutcome) + Send + Sync>;
131
132pub(crate) fn attempt_delivery(err: &HttpError) -> AttemptDelivery {
134 match err {
135 HttpError::Transport { connect_phase, .. } => {
136 if *connect_phase {
137 AttemptDelivery::NotSent
138 } else {
139 AttemptDelivery::Uncertain
140 }
141 }
142 HttpError::Timeout { phase, .. } => {
143 if *phase == Phase::Connect {
144 AttemptDelivery::NotSent
145 } else {
146 AttemptDelivery::Uncertain
147 }
148 }
149 HttpError::Cancelled => AttemptDelivery::Uncertain,
150 _ => AttemptDelivery::NotSent,
151 }
152}
153
154#[derive(Clone, Default)]
181pub enum ProxyMode {
182 #[default]
184 None,
185 Environment,
187 Explicit(String),
191 System,
215 SystemForUrl(String),
233}
234
235impl fmt::Debug for ProxyMode {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 ProxyMode::None => f.write_str("None"),
239 ProxyMode::Environment => f.write_str("Environment"),
240 ProxyMode::Explicit(url) => write!(f, "Explicit({})", redact_url(url)),
241 ProxyMode::System => f.write_str("System"),
242 ProxyMode::SystemForUrl(url) => write!(f, "SystemForUrl({})", redact_url(url)),
243 }
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
264pub enum TrustRoots {
265 #[default]
269 Platform,
270 Bundled,
274}
275
276fn blocking_tls_config(trust: TrustRoots) -> ureq::tls::TlsConfig {
279 let roots = match trust {
280 TrustRoots::Platform => ureq::tls::RootCerts::PlatformVerifier,
281 TrustRoots::Bundled => {
282 let extra = crate::trust::test_roots();
283 if extra.is_empty() {
284 ureq::tls::RootCerts::WebPki
285 } else {
286 ureq::tls::RootCerts::Specific(std::sync::Arc::new(
287 extra
288 .iter()
289 .map(|d| ureq::tls::Certificate::from_der(d).to_owned())
290 .collect(),
291 ))
292 }
293 }
294 };
295 ureq::tls::TlsConfig::builder()
296 .provider(ureq::tls::TlsProvider::Rustls)
297 .root_certs(roots)
298 .build()
299}
300
301impl TrustRoots {
302 pub fn verifier_kind(self) -> &'static str {
304 match self {
305 TrustRoots::Platform => "platform",
306 TrustRoots::Bundled => "bundled",
307 }
308 }
309}
310
311pub(crate) struct ResolvedProxy {
313 pub(crate) host: String,
314 pub(crate) port: u16,
315 pub(crate) ureq: ureq::Proxy,
316 #[cfg_attr(not(feature = "async"), allow(dead_code))]
318 pub(crate) url: String,
319 #[allow(dead_code)]
321 pub(crate) no_proxy: Vec<String>,
322 pub(crate) from_env: bool,
324}
325
326#[derive(Clone, Debug, PartialEq, Eq)]
330pub enum ProxyModeUsed {
331 Direct,
334 Environment,
337 Explicit,
339 System,
341 SystemFallbackEnvironment { reason: String },
344 SystemFallbackDirect { reason: String },
346 SystemForUrl,
349}
350
351#[derive(Clone, Debug, PartialEq, Eq)]
354pub struct ProxyDiagnostic {
355 pub endpoint: String,
357 pub from_environment: bool,
360 pub endpoint_exempt_from_class_check: bool,
364}
365
366impl ProxyDiagnostic {
367 pub(crate) fn of(p: &ResolvedProxy) -> Self {
368 Self {
369 endpoint: format!("{}://{}:{}", p.scheme(), p.host, p.port),
370 from_environment: p.from_env,
371 endpoint_exempt_from_class_check: true,
372 }
373 }
374}
375
376impl ResolvedProxy {
377 fn scheme(&self) -> &'static str {
378 if self.ureq.protocol() == ureq::ProxyProtocol::Https {
379 "https"
380 } else {
381 "http"
382 }
383 }
384}
385
386const PROXY_ENV: [&str; 6] = [
387 "ALL_PROXY",
388 "all_proxy",
389 "HTTPS_PROXY",
390 "https_proxy",
391 "HTTP_PROXY",
392 "http_proxy",
393];
394
395fn build_proxy(url: &str, no_proxy: &[String]) -> Result<ResolvedProxy, HttpError> {
397 let invalid = || HttpError::InvalidRequest("invalid proxy URL".into());
398 let base = ureq::Proxy::new(url).map_err(|_| invalid())?;
399 if !matches!(
400 base.protocol(),
401 ureq::ProxyProtocol::Http | ureq::ProxyProtocol::Https
402 ) {
403 return Err(invalid());
404 }
405 let mut builder = ureq::Proxy::builder(base.protocol())
406 .host(base.host())
407 .port(base.port())
408 .resolve_target(false);
409 if let Some(user) = base.username() {
410 builder = builder.username(user);
411 }
412 if let Some(pass) = base.password() {
413 builder = builder.password(pass);
414 }
415 for expr in no_proxy {
416 builder = builder.no_proxy(expr);
417 }
418 let ureq = builder.build().map_err(|_| invalid())?;
419 Ok(ResolvedProxy {
420 host: normalize_host(ureq.host()),
421 port: ureq.port(),
422 ureq,
423 url: url.to_string(),
424 no_proxy: no_proxy.to_vec(),
425 from_env: false,
426 })
427}
428
429pub(crate) enum Routing {
433 Fixed(Option<ResolvedProxy>),
434 PerRequest(Arc<SystemRoutes>),
435}
436
437pub(crate) struct SystemRoutes {
441 settings: crate::system_proxy::SystemProxySettings,
442 http: Option<ResolvedProxy>,
443 https: Option<ResolvedProxy>,
444}
445
446impl SystemRoutes {
447 pub(crate) fn new(
450 settings: &crate::system_proxy::SystemProxySettings,
451 ) -> Result<Self, HttpError> {
452 let raw = settings.proxy.as_deref().unwrap_or_default().trim();
453 let (http, https) = crate::system_proxy::endpoints_by_scheme(raw);
454 let build = |url: Option<String>| -> Result<Option<ResolvedProxy>, HttpError> {
455 url.map(|url| {
456 build_proxy(&url, &[]).map(|mut proxy| {
457 proxy.from_env = true;
458 proxy
459 })
460 })
461 .transpose()
462 };
463 Ok(Self {
464 settings: settings.clone(),
465 http: build(http)?,
466 https: build(https)?,
467 })
468 }
469
470 pub(crate) fn route(&self, scheme: &str, host: &str) -> crate::system_proxy::UrlRoute {
472 crate::system_proxy::static_route_for_url(&self.settings, scheme, host)
473 .unwrap_or(crate::system_proxy::UrlRoute::Direct)
474 }
475
476 pub(crate) fn proxy_for(&self, scheme: &str, host: &str) -> Option<&ResolvedProxy> {
478 match self.route(scheme, host) {
479 crate::system_proxy::UrlRoute::Direct => None,
480 crate::system_proxy::UrlRoute::Proxy(_) => {
481 if scheme == "https" {
482 self.https.as_ref()
483 } else {
484 self.http.as_ref()
485 }
486 }
487 }
488 }
489
490 pub(crate) fn endpoints(&self) -> Vec<(String, u16)> {
492 [&self.http, &self.https]
493 .into_iter()
494 .flatten()
495 .map(|p| (p.host.clone(), p.port))
496 .collect()
497 }
498
499 #[cfg(feature = "async")]
501 pub(crate) fn http(&self) -> Option<&ResolvedProxy> {
502 self.http.as_ref()
503 }
504
505 #[cfg(feature = "async")]
506 pub(crate) fn https(&self) -> Option<&ResolvedProxy> {
507 self.https.as_ref()
508 }
509}
510
511pub(crate) fn uri_scheme_and_host(uri: &ureq::http::Uri) -> (String, String) {
514 (
515 uri.scheme_str().unwrap_or("").to_ascii_lowercase(),
516 normalize_host(uri.host().unwrap_or("")),
517 )
518}
519
520pub(crate) fn proxy_from_lookup(
522 get: &dyn Fn(&str) -> Option<String>,
523) -> Result<Option<ResolvedProxy>, HttpError> {
524 let no_proxy: Vec<String> = ["NO_PROXY", "no_proxy"]
525 .iter()
526 .find_map(|name| get(name).filter(|v| !v.is_empty()))
527 .map(|v| {
528 v.split(',')
529 .map(|e| e.trim().to_string())
530 .filter(|e| !e.is_empty())
531 .collect()
532 })
533 .unwrap_or_default();
534 for name in PROXY_ENV {
535 if let Some(url) = get(name).filter(|v| !v.trim().is_empty()) {
536 if let Ok(mut proxy) = build_proxy(url.trim(), &no_proxy) {
537 proxy.from_env = true;
538 return Ok(Some(proxy));
539 }
540 }
541 }
542 Ok(None)
543}
544
545impl ClientConfig {
546 pub(crate) fn resolved_routing_with_mode(&self) -> Result<(Routing, ProxyModeUsed), HttpError> {
550 let bypassed = self.proxy_bypass_first_party && self.address_policy.is_first_party();
551 if matches!(self.proxy, ProxyMode::System) && !bypassed {
552 let env = |name: &str| std::env::var(name).ok();
553 return resolve_system_routing(&crate::system_proxy::system_proxy_settings(), &env);
554 }
555 self.resolved_proxy_with_mode()
556 .map(|(proxy, used)| (Routing::Fixed(proxy), used))
557 }
558
559 #[allow(dead_code)]
561 pub(crate) fn resolved_proxy(&self) -> Result<Option<ResolvedProxy>, HttpError> {
562 self.resolved_proxy_with_mode().map(|(proxy, _)| proxy)
563 }
564
565 pub(crate) fn resolved_proxy_with_mode(
567 &self,
568 ) -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
569 if self.proxy_bypass_first_party && self.address_policy.is_first_party() {
570 return Ok((None, ProxyModeUsed::Direct));
571 }
572 let env = |name: &str| std::env::var(name).ok();
573 match &self.proxy {
574 ProxyMode::None => Ok((None, ProxyModeUsed::Direct)),
575 ProxyMode::Explicit(url) => {
576 build_proxy(url, &[]).map(|p| (Some(p), ProxyModeUsed::Explicit))
577 }
578 ProxyMode::Environment => {
579 let proxy = proxy_from_lookup(&env)?;
580 let used = if proxy.is_some() {
581 ProxyModeUsed::Environment
582 } else {
583 ProxyModeUsed::Direct
584 };
585 Ok((proxy, used))
586 }
587 ProxyMode::System => {
588 resolve_system(&crate::system_proxy::system_proxy_settings(), &env)
589 }
590 ProxyMode::SystemForUrl(url) => resolve_system_for_url(
591 &crate::system_proxy::system_proxy_settings(),
592 url,
593 &crate::system_proxy::winhttp_route_for_url,
594 &env,
595 ),
596 }
597 }
598}
599
600pub(crate) fn resolve_system_for_url(
603 settings: &crate::system_proxy::SystemProxySettings,
604 url: &str,
605 os: &dyn Fn(
606 &str,
607 &crate::system_proxy::SystemProxySettings,
608 ) -> Option<crate::system_proxy::UrlRoute>,
609 get: &dyn Fn(&str) -> Option<String>,
610) -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
611 use crate::system_proxy::UrlRoute;
612 let Some((scheme, host)) = crate::system_proxy::scheme_and_host(url) else {
613 return Err(HttpError::InvalidRequest(
614 "invalid URL for system proxy resolution".into(),
615 ));
616 };
617 let from_route =
618 |route: UrlRoute| -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
619 match route {
620 UrlRoute::Direct => Ok((None, ProxyModeUsed::SystemForUrl)),
621 UrlRoute::Proxy(proxy_url) => {
622 let mut proxy = build_proxy(&proxy_url, &[])?;
623 proxy.from_env = true;
624 Ok((Some(proxy), ProxyModeUsed::SystemForUrl))
625 }
626 }
627 };
628 let automatic = settings.auto_config || settings.auto_detect;
629 if automatic {
630 if let Some(route) = os(url, settings) {
631 return from_route(route);
632 }
633 if settings.auto_config {
635 return Err(HttpError::SystemProxyUnsupported {
636 element: "the proxy auto-config script could not be evaluated for this URL".into(),
637 });
638 }
639 }
640 if let Some(route) = crate::system_proxy::static_route_for_url(settings, &scheme, &host) {
641 return from_route(route);
642 }
643 let proxy = proxy_from_lookup(get)?;
644 let used = match (settings.auto_detect, proxy.is_some()) {
645 (true, true) => ProxyModeUsed::SystemFallbackEnvironment {
646 reason: AUTO_DETECT_FAILED_REASON.to_string(),
647 },
648 (true, false) => ProxyModeUsed::SystemFallbackDirect {
649 reason: AUTO_DETECT_FAILED_REASON.to_string(),
650 },
651 (false, true) => ProxyModeUsed::Environment,
652 (false, false) => ProxyModeUsed::Direct,
653 };
654 Ok((proxy, used))
655}
656
657pub(crate) const AUTO_DETECT_FAILED_REASON: &str =
659 "automatic detection found no proxy for this URL; no static proxy or PAC configured";
660
661pub(crate) const AUTO_DETECT_ONLY_REASON: &str =
663 "automatic detection only; no static proxy or PAC configured";
664
665pub(crate) fn resolve_system_routing(
669 settings: &crate::system_proxy::SystemProxySettings,
670 get: &dyn Fn(&str) -> Option<String>,
671) -> Result<(Routing, ProxyModeUsed), HttpError> {
672 if matches!(
673 crate::system_proxy::proxy_choice(settings),
674 crate::system_proxy::SystemProxyChoice::PerRequest
675 ) {
676 return Ok((
677 Routing::PerRequest(Arc::new(SystemRoutes::new(settings)?)),
678 ProxyModeUsed::System,
679 ));
680 }
681 resolve_system(settings, get).map(|(proxy, used)| (Routing::Fixed(proxy), used))
682}
683
684pub(crate) fn resolve_system(
694 settings: &crate::system_proxy::SystemProxySettings,
695 get: &dyn Fn(&str) -> Option<String>,
696) -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
697 let env_or_direct = |used_env: ProxyModeUsed,
698 used_direct: ProxyModeUsed|
699 -> Result<(Option<ResolvedProxy>, ProxyModeUsed), HttpError> {
700 let proxy = proxy_from_lookup(get)?;
701 let used = if proxy.is_some() {
702 used_env
703 } else {
704 used_direct
705 };
706 Ok((proxy, used))
707 };
708 if matches!(
709 crate::system_proxy::proxy_choice(settings),
710 crate::system_proxy::SystemProxyChoice::AutoDetectOnly
711 ) {
712 let reason = AUTO_DETECT_ONLY_REASON.to_string();
713 return env_or_direct(
714 ProxyModeUsed::SystemFallbackEnvironment {
715 reason: reason.clone(),
716 },
717 ProxyModeUsed::SystemFallbackDirect { reason },
718 );
719 }
720 match proxy_from_system(settings)? {
721 Some(proxy) => Ok((Some(proxy), ProxyModeUsed::System)),
722 None => env_or_direct(ProxyModeUsed::Environment, ProxyModeUsed::Direct),
723 }
724}
725
726pub(crate) fn proxy_from_system(
736 settings: &crate::system_proxy::SystemProxySettings,
737) -> Result<Option<ResolvedProxy>, HttpError> {
738 use crate::system_proxy::SystemProxyChoice;
739 match crate::system_proxy::proxy_choice(settings) {
740 SystemProxyChoice::None | SystemProxyChoice::AutoDetectOnly => Ok(None),
741 SystemProxyChoice::Single { url, no_proxy } => {
742 Ok(build_proxy(&url, &no_proxy).ok().map(|mut proxy| {
743 proxy.from_env = true;
744 proxy
745 }))
746 }
747 SystemProxyChoice::PerRequest => Err(HttpError::SystemProxyUnsupported {
749 element: "the proxy settings differ by scheme or host and need per-request routing"
750 .into(),
751 }),
752 SystemProxyChoice::Unsupported(element) => {
753 Err(HttpError::SystemProxyUnsupported { element })
754 }
755 }
756}
757
758#[derive(Clone)]
759pub struct ClientConfig {
760 pub user_agent: String,
761 pub connect_timeout: Option<Duration>,
763 pub request_timeout: Duration,
765 pub response_timeout: Option<Duration>,
768 pub recv_body_timeout: Option<Duration>,
771 pub stream_timeout: Option<Duration>,
773 pub idle_timeout: Option<Duration>,
781 pub max_redirects: u32,
783 pub max_redirects_will_error: bool,
787 pub max_response_bytes: usize,
788 pub retry: RetryPolicy,
789 pub on_retry: Option<RetryObserver>,
790 pub on_attempt: Option<AttemptHook>,
796 pub redact_values: Vec<String>,
799 pub address_policy: AddressPolicy,
803 pub proxy: ProxyMode,
806 pub proxy_bypass_first_party: bool,
809 pub trust: TrustRoots,
813 pub https_only: bool,
818}
819
820impl Default for ClientConfig {
821 fn default() -> Self {
822 Self {
823 user_agent: concat!("rightkit-http/", env!("CARGO_PKG_VERSION")).to_string(),
824 connect_timeout: Some(Duration::from_secs(10)),
825 request_timeout: Duration::from_secs(60),
826 response_timeout: Some(Duration::from_secs(60)),
827 recv_body_timeout: None,
828 stream_timeout: None,
829 idle_timeout: None,
830 max_redirects: 0,
831 max_redirects_will_error: true,
832 max_response_bytes: 32 * 1024 * 1024,
833 retry: RetryPolicy::default(),
834 on_retry: None,
835 on_attempt: None,
836 redact_values: Vec::new(),
837 address_policy: AddressPolicy::default(),
838 proxy: ProxyMode::None,
839 proxy_bypass_first_party: false,
840 trust: TrustRoots::Platform,
841 https_only: false,
842 }
843 }
844}
845
846impl ClientConfig {
847 pub fn first_party(hosts: Vec<String>) -> Self {
851 Self::default().address_policy(AddressPolicy::first_party(hosts))
852 }
853
854 pub fn legacy_ureq_parity(user_agent: impl Into<String>) -> Self {
860 Self {
861 user_agent: user_agent.into(),
862 proxy: ProxyMode::Environment,
863 trust: TrustRoots::Bundled,
864 max_redirects: 10,
865 retry: RetryPolicy::none(),
866 ..Self::default()
867 }
868 }
869
870 pub fn on_attempt(
872 mut self,
873 hook: impl Fn(&AttemptEvent) -> Result<(), String> + Send + Sync + 'static,
874 ) -> Self {
875 self.on_attempt = Some(Arc::new(hook));
876 self
877 }
878
879 pub fn address_policy(mut self, policy: AddressPolicy) -> Self {
881 self.address_policy = policy;
882 self
883 }
884
885 pub fn proxy(mut self, mode: ProxyMode) -> Self {
887 self.proxy = mode;
888 self
889 }
890
891 pub fn trust(mut self, trust: TrustRoots) -> Self {
893 self.trust = trust;
894 self
895 }
896
897 pub fn idle_timeout(mut self, idle: Option<Duration>) -> Self {
900 self.idle_timeout = idle;
901 self
902 }
903
904 pub fn https_only(mut self, https_only: bool) -> Self {
906 self.https_only = https_only;
907 self
908 }
909
910 pub fn proxy_bypass_first_party(mut self, bypass: bool) -> Self {
913 self.proxy_bypass_first_party = bypass;
914 self
915 }
916
917 pub fn redact_values(mut self, values: Vec<String>) -> Self {
919 self.redact_values
920 .extend(values.into_iter().filter(|v| !v.is_empty()));
921 self
922 }
923}
924
925#[derive(Clone)]
926pub struct Request {
927 pub method: Method,
928 pub url: String,
929 pub headers: Vec<(String, String)>,
930 pub body: Vec<u8>,
931 pub timeout: Option<Duration>,
933 pub retry: Option<RetryPolicy>,
934 pub retry_unsafe_statuses: bool,
940 pub(crate) max_response_bytes: Option<usize>,
941 pub(crate) deadline: Option<Duration>,
942 pub(crate) cancel: Option<CancelToken>,
943 pub(crate) secret_values: Vec<String>,
944 pub(crate) headers_only: bool,
945 pub(crate) credential_scope: Option<crate::netpolicy::CredentialScope>,
946 pub(crate) approved_redirect_origins: Option<crate::netpolicy::ApprovedOrigins>,
947}
948
949impl fmt::Debug for Request {
950 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951 let headers: Vec<(String, String)> = self
952 .headers
953 .iter()
954 .map(|(k, v)| {
955 let shown = if redirect::is_secret_header_name(k) {
956 REDACTED.to_string()
957 } else {
958 redact_str(&redact_urls_in_text(v), &sorted_values(&self.secret_values))
959 };
960 (k.clone(), shown)
961 })
962 .collect();
963 f.debug_struct("Request")
964 .field("method", &self.method)
965 .field(
966 "url",
967 &redact_str(&redact_url(&self.url), &self.secret_values),
968 )
969 .field("headers", &headers)
970 .field("body_len", &self.body.len())
971 .field("timeout", &self.timeout)
972 .field("retry", &self.retry)
973 .field("retry_unsafe_statuses", &self.retry_unsafe_statuses)
974 .field("max_response_bytes", &self.max_response_bytes)
975 .field("deadline", &self.deadline)
976 .field("cancel", &self.cancel.is_some())
977 .field("headers_only", &self.headers_only)
978 .field("approved_redirect_origins", &self.approved_redirect_origins)
979 .finish()
980 }
981}
982
983impl Request {
984 pub fn new(method: Method, url: impl Into<String>) -> Self {
985 Self {
986 method,
987 url: url.into(),
988 headers: Vec::new(),
989 body: Vec::new(),
990 timeout: None,
991 retry: None,
992 retry_unsafe_statuses: false,
993 max_response_bytes: None,
994 deadline: None,
995 cancel: None,
996 secret_values: Vec::new(),
997 headers_only: false,
998 credential_scope: None,
999 approved_redirect_origins: None,
1000 }
1001 }
1002 pub fn get(url: impl Into<String>) -> Self {
1003 Self::new(Method::Get, url)
1004 }
1005 pub fn post(url: impl Into<String>) -> Self {
1006 Self::new(Method::Post, url)
1007 }
1008 pub fn put(url: impl Into<String>) -> Self {
1009 Self::new(Method::Put, url)
1010 }
1011 pub fn delete(url: impl Into<String>) -> Self {
1012 Self::new(Method::Delete, url)
1013 }
1014 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1016 self.headers.push((name.into(), value.into()));
1017 self
1018 }
1019 pub fn set_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1022 let name = name.into();
1023 let value = value.into();
1024 let position = self
1025 .headers
1026 .iter()
1027 .position(|(k, _)| k.eq_ignore_ascii_case(&name));
1028 self.headers.retain(|(k, _)| !k.eq_ignore_ascii_case(&name));
1029 match position {
1030 Some(i) => self.headers.insert(i, (name, value)),
1031 None => self.headers.push((name, value)),
1032 }
1033 self
1034 }
1035 pub fn secret_header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
1038 let value = value.into();
1039 let mut req = self;
1040 if !value.is_empty() {
1041 req.secret_values.push(value.clone());
1042 }
1043 req.set_header(name, value)
1044 }
1045 pub fn bearer(mut self, token: &str) -> Self {
1047 if !token.is_empty() {
1048 self.secret_values.push(token.to_string());
1049 }
1050 self.header("Authorization", format!("Bearer {token}"))
1051 }
1052 pub fn redact_value(mut self, value: impl Into<String>) -> Self {
1056 let value = value.into();
1057 if !value.is_empty() {
1058 self.secret_values.push(value);
1059 }
1060 self
1061 }
1062 pub fn secret_form(mut self, pairs: &[(&str, &str)], secret_names: &[&str]) -> Self {
1066 for (name, value) in pairs {
1067 if secret_names.iter().any(|s| s.eq_ignore_ascii_case(name)) && !value.is_empty() {
1068 self.secret_values.push((*value).to_string());
1069 }
1070 }
1071 self.form(pairs)
1072 }
1073 pub fn scrub(&self, text: &str) -> String {
1077 redact_str(text, &sorted_values(&self.secret_values))
1078 }
1079 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
1080 self.body = body.into();
1081 self
1082 }
1083 pub fn json<T: Serialize>(mut self, value: &T) -> Result<Self, HttpError> {
1084 self.body = serde_json::to_vec(value).map_err(json_error)?;
1085 self.headers
1086 .push(("Content-Type".into(), "application/json".into()));
1087 Ok(self)
1088 }
1089 pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
1091 self.body = form_encode(pairs).into_bytes();
1092 self.headers.push((
1093 "Content-Type".into(),
1094 "application/x-www-form-urlencoded".into(),
1095 ));
1096 self
1097 }
1098 pub fn timeout(mut self, d: Duration) -> Self {
1101 self.timeout = Some(d);
1102 self
1103 }
1104 pub fn deadline(mut self, total: Duration) -> Self {
1108 self.deadline = Some(total);
1109 self
1110 }
1111 pub fn retry(mut self, policy: RetryPolicy) -> Self {
1112 self.retry = Some(policy);
1113 self
1114 }
1115 pub fn retry_unsafe_statuses(mut self, allow: bool) -> Self {
1117 self.retry_unsafe_statuses = allow;
1118 self
1119 }
1120 pub fn max_response_bytes(mut self, limit: usize) -> Self {
1122 self.max_response_bytes = Some(limit);
1123 self
1124 }
1125 pub fn headers_only(mut self) -> Self {
1130 self.headers_only = true;
1131 self
1132 }
1133 pub fn cancel(mut self, token: CancelToken) -> Self {
1136 self.cancel = Some(token);
1137 self
1138 }
1139 pub fn credential_scope(mut self, scope: crate::netpolicy::CredentialScope) -> Self {
1145 self.credential_scope = Some(scope);
1146 self
1147 }
1148 pub fn approved_redirect_origins(mut self, origins: crate::netpolicy::ApprovedOrigins) -> Self {
1157 self.approved_redirect_origins = Some(origins);
1158 self
1159 }
1160}
1161
1162pub fn percent_encode(input: &str) -> String {
1164 let mut out = String::with_capacity(input.len());
1165 for b in input.bytes() {
1166 match b {
1167 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1168 out.push(b as char)
1169 }
1170 b' ' => out.push('+'),
1171 _ => out.push_str(&format!("%{b:02X}")),
1172 }
1173 }
1174 out
1175}
1176
1177pub fn percent_decode(input: &str) -> String {
1178 let bytes = input.as_bytes();
1179 let mut out = Vec::with_capacity(bytes.len());
1180 let mut i = 0;
1181 while i < bytes.len() {
1182 match bytes[i] {
1183 b'+' => out.push(b' '),
1184 b'%' if i + 2 < bytes.len() => {
1185 let hi = (bytes[i + 1] as char).to_digit(16);
1186 let lo = (bytes[i + 2] as char).to_digit(16);
1187 match (hi, lo) {
1188 (Some(h), Some(l)) => {
1189 out.push((h * 16 + l) as u8);
1190 i += 2;
1191 }
1192 _ => out.push(b'%'),
1193 }
1194 }
1195 b => out.push(b),
1196 }
1197 i += 1;
1198 }
1199 String::from_utf8_lossy(&out).into_owned()
1200}
1201
1202pub fn percent_encode_component(input: &str) -> String {
1206 let mut out = String::with_capacity(input.len());
1207 for b in input.bytes() {
1208 match b {
1209 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1210 out.push(b as char)
1211 }
1212 _ => out.push_str(&format!("%{b:02X}")),
1213 }
1214 }
1215 out
1216}
1217
1218pub fn percent_decode_component(input: &str) -> String {
1221 let bytes = input.as_bytes();
1222 let mut out = Vec::with_capacity(bytes.len());
1223 let mut i = 0;
1224 while i < bytes.len() {
1225 if bytes[i] == b'%' && i + 2 < bytes.len() {
1226 let hi = (bytes[i + 1] as char).to_digit(16);
1227 let lo = (bytes[i + 2] as char).to_digit(16);
1228 if let (Some(h), Some(l)) = (hi, lo) {
1229 out.push((h * 16 + l) as u8);
1230 i += 3;
1231 continue;
1232 }
1233 }
1234 out.push(bytes[i]);
1235 i += 1;
1236 }
1237 String::from_utf8_lossy(&out).into_owned()
1238}
1239
1240pub fn form_encode(pairs: &[(&str, &str)]) -> String {
1241 pairs
1242 .iter()
1243 .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
1244 .collect::<Vec<_>>()
1245 .join("&")
1246}
1247
1248#[derive(Clone)]
1249pub struct Response {
1250 pub status: u16,
1251 pub headers: Vec<(String, String)>,
1253 pub body: Vec<u8>,
1254 pub attempts: u32,
1255 pub(crate) final_url: String,
1257 pub(crate) redact: Vec<String>,
1260}
1261
1262impl Response {
1263 pub fn header(&self, name: &str) -> Option<&str> {
1264 header_of(&self.headers, name)
1265 }
1266 pub fn header_all<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a str> + 'a {
1269 header_all_of(&self.headers, name)
1270 }
1271 pub fn final_url(&self) -> &str {
1274 &self.final_url
1275 }
1276 pub fn retry_after(&self) -> Option<Duration> {
1278 self.header("retry-after").and_then(parse_retry_after)
1279 }
1280 pub fn text(&self) -> String {
1281 String::from_utf8_lossy(&self.body).into_owned()
1282 }
1283 pub fn json<T: DeserializeOwned>(&self) -> Result<T, HttpError> {
1286 serde_json::from_slice(&self.body).map_err(json_error)
1287 }
1288}
1289
1290pub(crate) fn json_error(e: serde_json::Error) -> HttpError {
1292 HttpError::Json(format!(
1293 "{:?} error at line {} column {}",
1294 e.classify(),
1295 e.line(),
1296 e.column()
1297 ))
1298}
1299
1300impl fmt::Debug for Response {
1301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1302 f.debug_struct("Response")
1303 .field("status", &self.status)
1304 .field("headers", &redact_headers_with(&self.headers, &self.redact))
1305 .field("body_len", &self.body.len())
1306 .field("attempts", &self.attempts)
1307 .finish()
1308 }
1309}
1310
1311#[derive(Clone)]
1314pub struct ResponseHead {
1315 pub status: u16,
1316 pub headers: Vec<(String, String)>,
1318 pub final_url: String,
1321 pub hops: u32,
1323 pub attempts: u32,
1325 pub(crate) redact: Vec<String>,
1327}
1328
1329impl ResponseHead {
1330 pub fn header(&self, name: &str) -> Option<&str> {
1331 header_of(&self.headers, name)
1332 }
1333 pub fn header_all<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a str> + 'a {
1335 header_all_of(&self.headers, name)
1336 }
1337}
1338
1339impl fmt::Debug for ResponseHead {
1340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1341 f.debug_struct("ResponseHead")
1342 .field("status", &self.status)
1343 .field("headers", &redact_headers_with(&self.headers, &self.redact))
1344 .field(
1345 "final_url",
1346 &redact_str(&redact_url(&self.final_url), &self.redact),
1347 )
1348 .field("hops", &self.hops)
1349 .field("attempts", &self.attempts)
1350 .finish()
1351 }
1352}
1353
1354pub struct StreamResponse {
1355 pub status: u16,
1356 pub headers: Vec<(String, String)>,
1357 pub attempts: u32,
1358 reader: Box<dyn Read + Send>,
1359}
1360
1361impl StreamResponse {
1362 pub fn header(&self, name: &str) -> Option<&str> {
1363 header_of(&self.headers, name)
1364 }
1365 pub fn header_all<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a str> + 'a {
1367 header_all_of(&self.headers, name)
1368 }
1369 pub fn copy_to<W: io::Write>(
1373 self,
1374 writer: &mut W,
1375 max_bytes: u64,
1376 mut progress: impl FnMut(u64),
1377 ) -> Result<u64, HttpError> {
1378 let mut reader = self.reader;
1379 let mut buf = vec![0u8; 64 * 1024];
1380 let mut total: u64 = 0;
1381 loop {
1382 let n = match reader.read(&mut buf) {
1383 Ok(0) => break,
1384 Ok(n) => n,
1385 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
1386 Err(e) if is_cancelled_io(&e) => return Err(HttpError::Cancelled),
1387 Err(e) if e.kind() == io::ErrorKind::TimedOut => {
1388 return Err(HttpError::timeout(e.to_string(), Phase::Body))
1389 }
1390 Err(e) => return Err(HttpError::transport(e.to_string(), Phase::Body)),
1391 };
1392 total = total.saturating_add(n as u64);
1393 if total > max_bytes {
1394 return Err(HttpError::BodyTooLarge {
1395 limit: max_bytes as usize,
1396 });
1397 }
1398 writer
1399 .write_all(&buf[..n])
1400 .map_err(|e| HttpError::Stream(format!("write failed: {e}")))?;
1401 progress(total);
1402 }
1403 Ok(total)
1404 }
1405 pub fn into_reader(self) -> Box<dyn Read + Send> {
1406 self.reader
1407 }
1408 pub fn sse(self) -> SseReader<Box<dyn Read + Send>> {
1410 SseReader::new(self.reader)
1411 }
1412}
1413
1414pub(crate) fn header_of<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
1415 headers
1416 .iter()
1417 .find(|(k, _)| k.eq_ignore_ascii_case(name))
1418 .map(|(_, v)| v.as_str())
1419}
1420
1421pub(crate) fn header_all_of<'a>(
1423 headers: &'a [(String, String)],
1424 name: &str,
1425) -> impl Iterator<Item = &'a str> + 'a {
1426 let name = name.to_string();
1427 headers
1428 .iter()
1429 .filter(move |(k, _)| k.eq_ignore_ascii_case(&name))
1430 .map(|(_, v)| v.as_str())
1431}
1432
1433#[derive(Clone)]
1434pub struct Client {
1435 agent: ureq::Agent,
1436 config: ClientConfig,
1437 on_attempt_outcome: Option<AttemptOutcomeObserver>,
1438 proxy: Option<ureq::Proxy>,
1440 routes: Option<Arc<SystemRoutes>>,
1442 proxy_diagnostic: Option<ProxyDiagnostic>,
1443 proxy_mode_used: ProxyModeUsed,
1445 config_error: Option<String>,
1447 compression: bool,
1449 system_proxy_unsupported: Option<String>,
1453}
1454
1455pub(crate) struct Raw {
1456 pub(crate) status: u16,
1457 pub(crate) headers: Vec<(String, String)>,
1458 pub(crate) body: ureq::Body,
1459 pub(crate) attempts: u32,
1460 pub(crate) final_url: String,
1462 pub(crate) hops: u32,
1464}
1465
1466impl Client {
1467 pub fn new(config: ClientConfig) -> Self {
1470 match Self::try_new(config.clone()) {
1471 Ok(client) => client,
1472 Err(e) => {
1473 let mut direct = config;
1474 direct.proxy = ProxyMode::None;
1475 let mut client = Self::build(direct, Routing::Fixed(None), ProxyModeUsed::Direct);
1476 if let HttpError::SystemProxyUnsupported { element } = &e {
1477 client.system_proxy_unsupported = Some(element.clone());
1478 }
1479 client.config_error = Some(e.to_string());
1480 client
1481 }
1482 }
1483 }
1484
1485 pub fn try_new(config: ClientConfig) -> Result<Self, HttpError> {
1487 let (routing, used) = config.resolved_routing_with_mode()?;
1488 Ok(Self::build(config, routing, used))
1489 }
1490
1491 fn build(config: ClientConfig, routing: Routing, used: ProxyModeUsed) -> Self {
1492 let (proxy, routes) = match routing {
1493 Routing::Fixed(proxy) => (proxy, None),
1494 Routing::PerRequest(routes) => (None, Some(routes)),
1495 };
1496 let builder = ureq::Agent::config_builder()
1499 .http_status_as_error(false)
1500 .max_redirects(0)
1501 .user_agent(config.user_agent.clone())
1502 .timeout_connect(config.connect_timeout)
1503 .timeout_resolve(config.connect_timeout)
1504 .tls_config(blocking_tls_config(config.trust));
1505 let diagnostic = proxy.as_ref().map(ProxyDiagnostic::of);
1506 let (builder, endpoint, proxy) = match proxy {
1507 Some(p) => (
1508 builder.proxy(Some(p.ureq.clone())),
1509 Some((p.host, p.port)),
1510 Some(p.ureq),
1511 ),
1512 None => (builder.proxy(None), None, None),
1513 };
1514 let mut proxies: Vec<(String, u16)> = endpoint.into_iter().collect();
1516 if let Some(routes) = &routes {
1517 proxies.extend(routes.endpoints());
1518 }
1519 let agent = ureq::Agent::with_parts(
1523 builder.build(),
1524 ureq::unversioned::transport::Connector::chain(
1525 ureq::unversioned::transport::Connector::chain(
1526 ConnectStart,
1527 ureq::unversioned::transport::DefaultConnector::default(),
1528 ),
1529 ConnectDone,
1530 ),
1531 VettingResolver {
1532 policy: Arc::new(config.address_policy.clone()),
1533 proxies,
1534 lookup_limit: config.connect_timeout.unwrap_or(Duration::from_secs(10)),
1535 },
1536 );
1537 Self {
1538 agent,
1539 config,
1540 on_attempt_outcome: None,
1541 proxy,
1542 routes,
1543 proxy_diagnostic: diagnostic,
1544 proxy_mode_used: used,
1545 config_error: None,
1546 compression: true,
1547 system_proxy_unsupported: None,
1548 }
1549 }
1550
1551 pub fn on_attempt_outcome(
1557 mut self,
1558 observer: impl Fn(&AttemptOutcome) + Send + Sync + 'static,
1559 ) -> Self {
1560 self.on_attempt_outcome = Some(Arc::new(observer));
1561 self
1562 }
1563
1564 pub fn compression(mut self, on: bool) -> Self {
1569 self.compression = on;
1570 self
1571 }
1572
1573 pub fn verifier_kind(&self) -> &'static str {
1576 self.config.trust.verifier_kind()
1577 }
1578
1579 pub fn proxy_diagnostic(&self) -> Option<&ProxyDiagnostic> {
1583 self.proxy_diagnostic.as_ref()
1584 }
1585
1586 pub fn proxy_mode_used(&self) -> &ProxyModeUsed {
1589 &self.proxy_mode_used
1590 }
1591
1592 pub fn first_party(hosts: Vec<String>) -> Self {
1595 Self::new(ClientConfig::first_party(hosts))
1596 }
1597
1598 pub fn config(&self) -> &ClientConfig {
1599 &self.config
1600 }
1601
1602 pub fn send(&self, req: &Request) -> Result<Response, HttpError> {
1604 self.send_checked(req, None)
1605 }
1606
1607 pub fn send_with_flag(&self, req: &Request, flag: &AtomicBool) -> Result<Response, HttpError> {
1615 self.send_checked(req, Some(flag))
1616 }
1617
1618 pub fn send_any_with_flag(
1620 &self,
1621 req: &Request,
1622 flag: &AtomicBool,
1623 ) -> Result<Response, HttpError> {
1624 self.send_any_checked(req, Some(flag))
1625 }
1626
1627 fn send_checked(
1628 &self,
1629 req: &Request,
1630 flag: Option<&AtomicBool>,
1631 ) -> Result<Response, HttpError> {
1632 let resp = self.send_any_checked(req, flag)?;
1633 if (200..300).contains(&resp.status) {
1634 Ok(resp)
1635 } else {
1636 let values = self.redaction_values(req);
1637 Err(scrub_error(
1638 status_error(resp.status, &resp.headers, resp.body, resp.attempts),
1639 &values,
1640 ))
1641 }
1642 }
1643
1644 pub fn send_any(&self, req: &Request) -> Result<Response, HttpError> {
1646 self.send_any_checked(req, None)
1647 }
1648
1649 fn send_any_checked(
1650 &self,
1651 req: &Request,
1652 flag: Option<&AtomicBool>,
1653 ) -> Result<Response, HttpError> {
1654 let timeout = req.timeout.unwrap_or(self.config.request_timeout);
1655 let cancel = flag.or_else(|| request_cancel(req));
1656 let mut raw = self.execute(req, Some(timeout), cancel)?;
1657 let values = self.redaction_values(req);
1658 if req.headers_only {
1659 return Ok(Response {
1661 status: raw.status,
1662 headers: raw.headers,
1663 body: Vec::new(),
1664 attempts: raw.attempts,
1665 final_url: raw.final_url,
1666 redact: values,
1667 });
1668 }
1669 let limit = req
1670 .max_response_bytes
1671 .unwrap_or(self.config.max_response_bytes);
1672 let mut body_reader = raw.body.as_reader();
1673 let mut reader = CheckedReader {
1674 inner: &mut body_reader,
1675 cancel,
1676 };
1677 let body = read_limited(&mut reader, limit).map_err(|e| scrub_error(e, &values))?;
1678 Ok(Response {
1679 status: raw.status,
1680 headers: raw.headers,
1681 body,
1682 attempts: raw.attempts,
1683 final_url: raw.final_url,
1684 redact: values,
1685 })
1686 }
1687
1688 pub fn send_headers(&self, req: &Request) -> Result<ResponseHead, HttpError> {
1693 let timeout = req.timeout.unwrap_or(self.config.request_timeout);
1694 let raw = self.execute(req, Some(timeout), request_cancel(req))?;
1695 let Raw {
1696 status,
1697 headers,
1698 body,
1699 attempts,
1700 final_url,
1701 hops,
1702 ..
1703 } = raw;
1704 drop(body);
1705 Ok(ResponseHead {
1706 status,
1707 headers,
1708 final_url,
1709 hops,
1710 attempts,
1711 redact: self.redaction_values(req),
1712 })
1713 }
1714
1715 pub fn stream_any(&self, req: &Request) -> Result<StreamResponse, HttpError> {
1720 let total = req.timeout.or(self.config.stream_timeout);
1721 let raw = self.execute(req, total, request_cancel(req))?;
1722 let values = self.redaction_values(req);
1723 Ok(self.wrap_stream(raw, req, values))
1724 }
1725
1726 fn wrap_stream(&self, raw: Raw, req: &Request, values: Vec<String>) -> StreamResponse {
1727 let body: Box<dyn Read + Send> = match self.config.idle_timeout {
1728 Some(idle) => Box::new(crate::idle::IdleReader::new(
1729 Box::new(raw.body.into_reader()),
1730 idle,
1731 req.cancel.clone(),
1732 )),
1733 None => Box::new(raw.body.into_reader()),
1734 };
1735 let guarded = GuardReader {
1736 inner: body,
1737 cancel: req.cancel.clone(),
1738 values,
1739 };
1740 StreamResponse {
1741 status: raw.status,
1742 headers: raw.headers,
1743 attempts: raw.attempts,
1744 reader: Box::new(guarded),
1745 }
1746 }
1747
1748 pub fn stream(&self, req: &Request) -> Result<StreamResponse, HttpError> {
1754 let cancel = request_cancel(req);
1755 let total = req.timeout.or(self.config.stream_timeout);
1756 let mut raw = self.execute(req, total, cancel)?;
1757 let values = self.redaction_values(req);
1758 if !(200..300).contains(&raw.status) {
1759 let mut body_reader = raw.body.as_reader();
1760 let mut reader = CheckedReader {
1761 inner: &mut body_reader,
1762 cancel,
1763 };
1764 let body = read_limited(&mut reader, MAX_ERROR_BODY_BYTES).unwrap_or_default();
1765 if is_set(cancel) {
1766 return Err(HttpError::Cancelled);
1767 }
1768 return Err(scrub_error(
1769 status_error(raw.status, &raw.headers, body, raw.attempts),
1770 &values,
1771 ));
1772 }
1773 Ok(self.wrap_stream(raw, req, values))
1774 }
1775
1776 pub fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T, HttpError> {
1777 let req = Request::get(url).header("Accept", "application/json");
1778 let values = self.redaction_values(&req);
1779 self.send(&req)?.json().map_err(|e| scrub_error(e, &values))
1780 }
1781
1782 pub(crate) fn redaction_values(&self, req: &Request) -> Vec<String> {
1784 redaction_values_for(&self.config, req)
1785 }
1786
1787 fn execute(
1790 &self,
1791 req: &Request,
1792 total: Option<Duration>,
1793 cancel: Option<&AtomicBool>,
1794 ) -> Result<Raw, HttpError> {
1795 let values = self.redaction_values(req);
1796 self.execute_inner(req, total, cancel, &values)
1797 .map_err(|e| scrub_error(e, &values))
1798 }
1799
1800 fn execute_inner(
1801 &self,
1802 req: &Request,
1803 total: Option<Duration>,
1804 cancel: Option<&AtomicBool>,
1805 values: &[String],
1806 ) -> Result<Raw, HttpError> {
1807 let policy = req
1808 .retry
1809 .clone()
1810 .unwrap_or_else(|| self.config.retry.clone());
1811 let max = policy.max_attempts.max(1);
1812 let deadline = req.deadline.map(|d| Instant::now() + d);
1813 let mut budgets = AttemptBudgets::new();
1814 let mut attempt = 0u32;
1815 loop {
1816 if is_set(cancel) {
1817 return Err(HttpError::Cancelled);
1818 }
1819 let attempt_total = match deadline {
1820 None => total,
1821 Some(dl) => {
1822 let remaining = dl.saturating_duration_since(Instant::now());
1823 if remaining.is_zero() {
1824 return Err(HttpError::timeout(
1825 format!("request deadline exceeded after {attempt} attempt(s)"),
1826 Phase::Headers,
1827 ));
1828 }
1829 Some(total.map_or(remaining, |t| t.min(remaining)))
1830 }
1831 };
1832 attempt += 1;
1833 if let Some(hook) = &self.config.on_attempt {
1834 hook(&AttemptEvent {
1835 url: redact_str(&redact_url(&req.url), values),
1836 method: req.method,
1837 attempt,
1838 max_attempts: max,
1839 })
1840 .map_err(|reason| {
1841 HttpError::InvalidRequest(format!("attempt {attempt} refused: {reason}"))
1842 })?;
1843 }
1844 let mut identity = None;
1845 let (reason, delay) =
1846 match self.once(req, attempt_total, values, attempt, &mut identity) {
1847 Ok(raw) => {
1848 let retryable = policy.is_retryable_status(raw.status)
1849 && (req.method.is_idempotent() || req.retry_unsafe_statuses);
1850 let retry_after =
1851 header_of(&raw.headers, "retry-after").and_then(parse_retry_after);
1852 let delay = if retryable && !policy.defers(retry_after) {
1853 budgets
1854 .status_failed(&policy, attempt)
1855 .map(|index| policy.delay(index, retry_after))
1856 .filter(|delay| !deadline_passes(deadline, *delay))
1857 } else {
1858 None
1859 };
1860 observe_attempt_outcome(
1862 &self.on_attempt_outcome,
1863 &identity,
1864 values,
1865 attempt,
1866 Some(raw.status),
1867 Some(&raw.headers),
1868 None,
1869 delay,
1870 );
1871 let Some(delay) = delay else {
1872 return Ok(Raw {
1873 attempts: attempt,
1874 ..raw
1875 });
1876 };
1877 let mut body = raw.body;
1879 let mut reader = body.as_reader();
1880 let mut checked = CheckedReader {
1881 inner: &mut reader,
1882 cancel,
1883 };
1884 let _ = read_limited(&mut checked, MAX_ERROR_BODY_BYTES);
1885 if is_set(cancel) {
1886 return Err(HttpError::Cancelled);
1887 }
1888 (format!("HTTP {}", raw.status), delay)
1889 }
1890 Err(err) => {
1891 let retryable = match &err {
1892 HttpError::Transport { connect_phase, .. } => {
1893 *connect_phase
1894 && identity.as_ref().is_some_and(|(_, _, hop, _)| *hop == 0)
1895 || req.method.is_idempotent()
1896 }
1897 HttpError::Timeout { .. } => req.method.is_idempotent(),
1898 _ => false,
1899 };
1900 let delay = if retryable {
1901 budgets
1902 .transport_failed(&policy, attempt)
1903 .map(|index| policy.delay(index, None))
1904 .filter(|delay| !deadline_passes(deadline, *delay))
1905 } else {
1906 None
1907 };
1908 observe_attempt_outcome(
1909 &self.on_attempt_outcome,
1910 &identity,
1911 values,
1912 attempt,
1913 None,
1914 None,
1915 Some(&err),
1916 delay,
1917 );
1918 let Some(delay) = delay else {
1919 return Err(err);
1920 };
1921 (err.to_string(), delay)
1922 }
1923 };
1924 if let Some(obs) = &self.config.on_retry {
1925 obs(&RetryEvent {
1926 url: redact_str(&redact_url(&req.url), values),
1927 retry: attempt,
1928 reason: redact_str(&redact_urls_in_text(&reason), values),
1929 delay,
1930 });
1931 }
1932 let delay = match deadline {
1935 Some(dl) => delay.min(dl.saturating_duration_since(Instant::now())),
1936 None => delay,
1937 };
1938 if !sleep_cancellable(delay, cancel) {
1939 return Err(HttpError::Cancelled);
1940 }
1941 }
1942 }
1943
1944 fn once(
1947 &self,
1948 req: &Request,
1949 total: Option<Duration>,
1950 secrets: &[String],
1951 attempt: u32,
1952 identity: &mut Option<(String, Method, u32, Option<AttemptDelivery>)>,
1953 ) -> Result<Raw, HttpError> {
1954 let started = Instant::now();
1955 let mut method = req.method;
1956 let mut url = req.url.clone();
1957 let mut headers = req.headers.clone();
1958 if !self.compression
1959 && !headers
1960 .iter()
1961 .any(|(k, _)| k.eq_ignore_ascii_case("accept-encoding"))
1962 {
1963 headers.push(("Accept-Encoding".to_string(), "identity".to_string()));
1964 }
1965 if let Some(scope) = &req.credential_scope {
1968 if !scope.allows(&url) {
1969 redirect::strip_cross_origin(&mut headers, secrets);
1970 }
1971 }
1972 let mut body = req.body.clone();
1973 let mut hops = 0u32;
1974 loop {
1975 let remaining = match total {
1976 None => None,
1977 Some(t) => {
1978 let left = t.saturating_sub(started.elapsed());
1979 if left.is_zero() {
1980 let phase = if hops > 0 {
1981 Phase::Redirect
1982 } else {
1983 Phase::Headers
1984 };
1985 return Err(HttpError::timeout("request", phase));
1986 }
1987 Some(left)
1988 }
1989 };
1990 *identity = Some((url.clone(), method, hops, None));
1991 let raw = match self.once_hop(method, &url, &headers, &body, remaining, hops > 0) {
1992 Ok(raw) => raw,
1993 Err(e) if hops > 0 => {
1994 if let Some(id) = identity.as_mut() {
1998 id.3 = Some(attempt_delivery(&e));
1999 }
2000 return Err(e.into_redirect_phase());
2001 }
2002 Err(e) => return Err(e),
2003 };
2004 if self.config.max_redirects == 0 || !redirect::is_redirect_status(raw.status) {
2005 return Ok(Raw {
2006 final_url: url,
2007 hops,
2008 ..raw
2009 });
2010 }
2011 let Some(location) = header_of(&raw.headers, "location") else {
2012 return Ok(Raw {
2013 final_url: url,
2014 hops,
2015 ..raw
2016 });
2017 };
2018 let Some(next_method) = redirect::redirect_method(raw.status, method) else {
2019 return Ok(Raw {
2020 final_url: url,
2021 hops,
2022 ..raw
2023 });
2024 };
2025 if hops < self.config.max_redirects || self.config.max_redirects_will_error {
2026 observe_attempt_outcome(
2027 &self.on_attempt_outcome,
2028 identity,
2029 secrets,
2030 attempt,
2031 Some(raw.status),
2032 Some(&raw.headers),
2033 None,
2034 None,
2035 );
2036 *identity = None;
2037 }
2038 if hops >= self.config.max_redirects {
2039 if self.config.max_redirects_will_error {
2040 return Err(HttpError::transport("too many redirects", Phase::Redirect));
2041 }
2042 return Ok(Raw {
2043 final_url: url,
2044 hops,
2045 ..raw
2046 });
2047 }
2048 hops += 1;
2049 let next_url = redirect::resolve_location(&url, location).ok_or_else(|| {
2050 HttpError::InvalidRequest("redirect with an unusable Location".into())
2051 })?;
2052 if let (Some(from), Some(to), Some(host)) = (
2053 redirect::scheme_of(&url),
2054 redirect::scheme_of(&next_url),
2055 redirect::host_of(&next_url),
2056 ) {
2057 redirect::require_https(self.config.https_only, &to, &host, true)?;
2060 self.config
2061 .address_policy
2062 .check_redirect_scheme(&from, &to, &host)
2063 .map_err(|d| HttpError::from(d).into_redirect_phase())?;
2064 }
2065 redirect::vet_redirect(&req.url, &next_url, req.approved_redirect_origins.as_ref())?;
2068 if redirect::origin(&url) != redirect::origin(&next_url) {
2069 redirect::strip_cross_origin(&mut headers, secrets);
2070 }
2071 if let Some(scope) = &req.credential_scope {
2072 if !scope.allows(&next_url) {
2073 redirect::strip_cross_origin(&mut headers, secrets);
2074 }
2075 }
2076 if next_method != method {
2077 body.clear();
2078 redirect::strip_body_headers(&mut headers);
2079 }
2080 method = next_method;
2081 url = next_url;
2082 }
2084 }
2085
2086 fn once_hop(
2087 &self,
2088 method: Method,
2089 url: &str,
2090 headers: &[(String, String)],
2091 body: &[u8],
2092 total: Option<Duration>,
2093 redirect_hop: bool,
2094 ) -> Result<Raw, HttpError> {
2095 let mut builder = ureq::http::Request::builder()
2096 .method(method.as_str())
2097 .uri(url);
2098 for (k, v) in headers {
2099 builder = builder.header(k.as_str(), v.as_str());
2100 }
2101 let bodiless =
2105 body.is_empty() && !matches!(method, Method::Post | Method::Put | Method::Patch);
2106 if bodiless {
2107 let request = builder
2108 .body(())
2109 .map_err(|_| HttpError::InvalidRequest("request could not be built".into()))?;
2110 self.run_hop(request, total, redirect_hop)
2111 } else {
2112 let request = builder
2113 .body(body.to_vec())
2114 .map_err(|_| HttpError::InvalidRequest("request could not be built".into()))?;
2115 self.run_hop(request, total, redirect_hop)
2116 }
2117 }
2118
2119 fn run_hop<S: ureq::AsSendBody>(
2121 &self,
2122 request: ureq::http::Request<S>,
2123 total: Option<Duration>,
2124 redirect_hop: bool,
2125 ) -> Result<Raw, HttpError> {
2126 if let Some(element) = &self.system_proxy_unsupported {
2127 return Err(HttpError::SystemProxyUnsupported {
2128 element: element.clone(),
2129 });
2130 }
2131 if let Some(reason) = &self.config_error {
2132 return Err(HttpError::InvalidRequest(reason.clone()));
2133 }
2134 redirect::require_https(
2137 self.config.https_only,
2138 request.uri().scheme_str().unwrap_or(""),
2139 request.uri().host().unwrap_or(""),
2140 redirect_hop,
2141 )?;
2142 let (via_proxy, hop_proxy) = self.vet_proxied_destination(request.uri())?;
2143 let _scope = ViaProxy::enter(via_proxy);
2144 let mut configured = self.agent.configure_request(request);
2145 if let Some(proxy) = hop_proxy {
2146 configured = configured.proxy(Some(proxy));
2148 }
2149 let configured = configured
2150 .timeout_global(total)
2151 .timeout_recv_response(self.config.response_timeout)
2152 .timeout_recv_body(self.config.recv_body_timeout)
2153 .build();
2154 CONNECT_STATE.with(|c| c.set(ConnectState::Idle));
2155 let response = self.agent.run(configured).map_err(|err| {
2156 let default = match CONNECT_STATE.with(|c| c.get()) {
2161 ConnectState::Started => Phase::Connect,
2162 _ => Phase::Headers,
2163 };
2164 map_ureq_error_in(err, default)
2165 })?;
2166 let status = response.status().as_u16();
2167 let headers = response
2168 .headers()
2169 .iter()
2170 .map(|(k, v)| {
2171 (
2172 k.as_str().to_ascii_lowercase(),
2173 String::from_utf8_lossy(v.as_bytes()).into_owned(),
2174 )
2175 })
2176 .collect();
2177 Ok(Raw {
2178 status,
2179 headers,
2180 body: response.into_body(),
2181 attempts: 1,
2182 final_url: String::new(),
2183 hops: 0,
2184 })
2185 }
2186}
2187
2188impl Client {
2189 fn vet_proxied_destination(
2195 &self,
2196 uri: &ureq::http::Uri,
2197 ) -> Result<(bool, Option<ureq::Proxy>), HttpError> {
2198 let policy = &self.config.address_policy;
2199 if let Some(routes) = &self.routes {
2200 let (scheme, host) = uri_scheme_and_host(uri);
2201 let Some(proxy) = routes.proxy_for(&scheme, &host) else {
2202 return Ok((false, None));
2205 };
2206 let raw_host = uri.host().unwrap_or("");
2207 policy.admit(&scheme, raw_host)?;
2208 policy.vet_via_proxy(raw_host)?;
2209 return Ok((true, Some(proxy.ureq.clone())));
2210 }
2211 let Some(proxy) = &self.proxy else {
2212 return Ok((false, None));
2213 };
2214 if proxy.is_no_proxy(uri) {
2215 return Ok((false, None));
2216 }
2217 let scheme = uri.scheme_str().unwrap_or("").to_ascii_lowercase();
2218 let raw_host = uri.host().unwrap_or("");
2219 policy.admit(&scheme, raw_host)?;
2220 policy.vet_via_proxy(raw_host)?;
2221 Ok((true, None))
2222 }
2223}
2224
2225fn sorted_values(values: &[String]) -> Vec<String> {
2226 let mut v: Vec<String> = values.iter().filter(|v| !v.is_empty()).cloned().collect();
2227 v.sort_by_key(|s| std::cmp::Reverse(s.len()));
2228 v
2229}
2230
2231pub(crate) fn redact_headers_for_debug(headers: &[(String, String)]) -> Vec<(String, String)> {
2235 headers
2236 .iter()
2237 .map(|(k, v)| {
2238 let shown = if redirect::is_secret_header_name(k) {
2239 REDACTED.to_string()
2240 } else if matches!(
2241 k.to_ascii_lowercase().as_str(),
2242 "location" | "content-location" | "refresh" | "link" | "referer"
2243 ) {
2244 redact_urls_in_text(&redact_url(v))
2245 } else {
2246 redact_urls_in_text(v)
2247 };
2248 (k.clone(), shown)
2249 })
2250 .collect()
2251}
2252
2253pub(crate) fn redact_headers_with(
2255 headers: &[(String, String)],
2256 values: &[String],
2257) -> Vec<(String, String)> {
2258 redact_headers_for_debug(headers)
2259 .into_iter()
2260 .map(|(k, v)| (k, redact_str(&v, values)))
2261 .collect()
2262}
2263
2264pub(crate) fn redaction_values_for(config: &ClientConfig, req: &Request) -> Vec<String> {
2267 let mut values: Vec<String> = config
2268 .redact_values
2269 .iter()
2270 .chain(req.secret_values.iter())
2271 .filter(|v| !v.is_empty())
2272 .cloned()
2273 .chain(url_secret_parts(&req.url))
2274 .collect();
2275 values.sort_by_key(|v| std::cmp::Reverse(v.len()));
2276 values.dedup();
2277 values
2278}
2279
2280#[allow(clippy::too_many_arguments)]
2283pub(crate) fn observe_attempt_outcome(
2284 observer: &Option<AttemptOutcomeObserver>,
2285 identity: &Option<(String, Method, u32, Option<AttemptDelivery>)>,
2286 values: &[String],
2287 attempt: u32,
2288 status: Option<u16>,
2289 headers: Option<&[(String, String)]>,
2290 error: Option<&HttpError>,
2291 next_delay: Option<Duration>,
2292) {
2293 let Some((url, method, redirect_hop, hop_delivery)) = identity else {
2294 return;
2295 };
2296 if matches!(
2297 error,
2298 Some(
2299 HttpError::InvalidRequest(_)
2300 | HttpError::AddressDenied { .. }
2301 | HttpError::HttpsRequired { .. }
2302 | HttpError::RedirectRefused { .. }
2303 | HttpError::SystemProxyUnsupported { .. }
2304 )
2305 ) {
2306 return;
2307 }
2308 let Some(observer) = observer else {
2309 return;
2310 };
2311 observer(&AttemptOutcome {
2312 url: redact_str(&redact_url(url), values),
2313 method: *method,
2314 attempt,
2315 redirect_hop: *redirect_hop,
2316 status,
2317 headers: headers
2318 .map(|h| redact_headers_with(h, values))
2319 .unwrap_or_default(),
2320 error_class: error.map(HttpError::class),
2321 error_phase: error.and_then(HttpError::phase),
2322 error: error.map(|e| redact_str(&redact_urls_in_text(&e.to_string()), values)),
2323 delivery: match error {
2324 Some(e) => hop_delivery.unwrap_or_else(|| attempt_delivery(e)),
2327 None => AttemptDelivery::Responded,
2328 },
2329 next_delay,
2330 });
2331}
2332
2333fn request_cancel(req: &Request) -> Option<&AtomicBool> {
2334 req.cancel.as_ref().map(CancelToken::as_atomic)
2335}
2336
2337fn deadline_passes(deadline: Option<Instant>, delay: Duration) -> bool {
2338 deadline.is_some_and(|dl| Instant::now() + delay >= dl)
2339}
2340
2341struct GuardReader {
2344 inner: Box<dyn Read + Send>,
2345 cancel: Option<CancelToken>,
2346 values: Vec<String>,
2347}
2348
2349impl Read for GuardReader {
2350 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
2351 if self.cancel.as_ref().is_some_and(|c| c.is_cancelled()) {
2352 return Err(crate::cancel::cancelled_io());
2353 }
2354 self.inner.read(buf).map_err(|e| {
2355 if is_cancelled_io(&e) {
2356 return e;
2357 }
2358 let text = e.to_string();
2359 let scrubbed = redact_str(&redact_urls_in_text(&text), &self.values);
2360 if scrubbed == text {
2361 e
2362 } else {
2363 io::Error::new(e.kind(), scrubbed)
2364 }
2365 })
2366 }
2367}
2368
2369#[derive(Clone, Copy, PartialEq, Eq)]
2370enum ConnectState {
2371 Idle,
2373 Started,
2375 Done,
2377}
2378
2379thread_local! {
2380 static CONNECT_STATE: std::cell::Cell<ConnectState> = const { std::cell::Cell::new(ConnectState::Idle) };
2383}
2384
2385#[derive(Debug)]
2387struct ConnectStart;
2388
2389impl ureq::unversioned::transport::Connector<()> for ConnectStart {
2390 type Out = ();
2391 fn connect(
2392 &self,
2393 _details: &ureq::unversioned::transport::ConnectionDetails,
2394 chained: Option<()>,
2395 ) -> Result<Option<()>, ureq::Error> {
2396 CONNECT_STATE.with(|c| c.set(ConnectState::Started));
2397 Ok(chained)
2398 }
2399}
2400
2401#[derive(Debug)]
2403struct ConnectDone;
2404
2405impl ureq::unversioned::transport::Connector<Box<dyn ureq::unversioned::transport::Transport>>
2406 for ConnectDone
2407{
2408 type Out = Box<dyn ureq::unversioned::transport::Transport>;
2409 fn connect(
2410 &self,
2411 _details: &ureq::unversioned::transport::ConnectionDetails,
2412 chained: Option<Self::Out>,
2413 ) -> Result<Option<Self::Out>, ureq::Error> {
2414 CONNECT_STATE.with(|c| c.set(ConnectState::Done));
2415 Ok(chained)
2416 }
2417}
2418
2419pub(crate) fn status_error(
2420 status: u16,
2421 headers: &[(String, String)],
2422 body: Vec<u8>,
2423 attempts: u32,
2424) -> HttpError {
2425 let mut body = body;
2426 body.truncate(MAX_ERROR_BODY_BYTES);
2427 HttpError::Status {
2428 status,
2429 body: String::from_utf8_lossy(&body).into_owned(),
2430 retry_after: header_of(headers, "retry-after").and_then(parse_retry_after),
2431 attempts,
2432 }
2433}
2434
2435pub(crate) fn redact_str(text: &str, values: &[String]) -> String {
2437 let mut out = text.to_string();
2438 for value in values {
2439 if !value.is_empty() && out.contains(value.as_str()) {
2440 out = out.replace(value.as_str(), REDACTED);
2441 }
2442 }
2443 out
2444}
2445
2446pub(crate) fn scrub_error(err: HttpError, values: &[String]) -> HttpError {
2448 let r = |s: String| redact_str(&redact_urls_in_text(&s), values);
2451 match err {
2452 HttpError::InvalidRequest(m) => HttpError::InvalidRequest(r(m)),
2453 HttpError::Transport {
2454 message,
2455 connect_phase,
2456 phase,
2457 } => HttpError::Transport {
2458 message: r(message),
2459 connect_phase,
2460 phase,
2461 },
2462 HttpError::Timeout { message, phase } => HttpError::Timeout {
2463 message: r(message),
2464 phase,
2465 },
2466 HttpError::Status {
2467 status,
2468 body,
2469 retry_after,
2470 attempts,
2471 } => HttpError::Status {
2472 status,
2473 body: redact_str(&body, values),
2474 retry_after,
2475 attempts,
2476 },
2477 HttpError::BodyTooLarge { limit } => HttpError::BodyTooLarge { limit },
2478 HttpError::Json(m) => HttpError::Json(r(m)),
2479 HttpError::Stream(m) => HttpError::Stream(r(m)),
2480 HttpError::Cancelled => HttpError::Cancelled,
2481 HttpError::SystemProxyUnsupported { element } => HttpError::SystemProxyUnsupported {
2482 element: element.clone(),
2483 },
2484 HttpError::HttpsRequired {
2485 host,
2486 scheme,
2487 redirect,
2488 } => HttpError::HttpsRequired {
2489 host: r(host),
2490 scheme,
2491 redirect,
2492 },
2493 HttpError::AddressDenied {
2494 host,
2495 addr,
2496 class,
2497 reason,
2498 } => HttpError::AddressDenied {
2499 host: r(host),
2500 addr,
2501 class,
2502 reason: r(reason),
2503 },
2504 HttpError::RedirectRefused { host, reason } => HttpError::RedirectRefused {
2505 host: r(host),
2506 reason,
2507 },
2508 }
2509}
2510
2511pub(crate) fn read_limited(reader: &mut dyn Read, limit: usize) -> Result<Vec<u8>, HttpError> {
2512 let mut out = Vec::new();
2513 reader
2514 .take(limit as u64 + 1)
2515 .read_to_end(&mut out)
2516 .map_err(|e| {
2517 if is_cancelled_io(&e) {
2518 return HttpError::Cancelled;
2519 }
2520 match e.kind() {
2521 std::io::ErrorKind::TimedOut => HttpError::timeout(e.to_string(), Phase::Body),
2522 _ => map_ureq_error_in(e.into(), Phase::Body),
2525 }
2526 })?;
2527 if out.len() > limit {
2528 return Err(HttpError::BodyTooLarge { limit });
2529 }
2530 Ok(out)
2531}
2532
2533fn timeout_phase(t: &ureq::Timeout, default: Phase) -> Phase {
2535 use ureq::Timeout as T;
2536 match t {
2537 T::Resolve | T::Connect => Phase::Connect,
2538 T::SendRequest | T::SendBody | T::RecvResponse => Phase::Headers,
2539 T::RecvBody => Phase::Body,
2540 _ => default,
2541 }
2542}
2543
2544pub(crate) fn map_ureq_error_in(err: ureq::Error, default: Phase) -> HttpError {
2547 use ureq::Error as E;
2548 if let E::Io(io) = &err {
2550 if let Some(inner) = io.get_ref() {
2551 if let Some(denial) = denial_in_chain(inner) {
2552 return denial.into();
2553 }
2554 }
2555 }
2556 match err {
2557 E::Timeout(t) => HttpError::timeout(format!("{t:?}"), timeout_phase(&t, default)),
2558 E::TooManyRedirects => HttpError::transport("too many redirects", Phase::Redirect),
2559 E::HostNotFound => HttpError::transport("host not found", Phase::Connect),
2560 E::ConnectionFailed => HttpError::transport("connection failed", Phase::Connect),
2561 E::Io(io)
2562 if matches!(
2563 io.kind(),
2564 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
2565 ) =>
2566 {
2567 HttpError::transport(io.to_string(), Phase::Connect)
2568 }
2569 E::Io(io) => HttpError::transport(io.to_string(), default),
2570 E::BadUri(m) => HttpError::InvalidRequest(m),
2571 E::Http(e) => HttpError::InvalidRequest(e.to_string()),
2572 other => HttpError::transport(other.to_string(), default),
2573 }
2574}
2575
2576#[cfg(test)]
2577mod tests {
2578 use super::*;
2579
2580 #[test]
2581 fn legacy_ureq_parity_sets_only_the_ureq_fields() {
2582 let cfg = ClientConfig::legacy_ureq_parity("heardright/1.0");
2583 let base = ClientConfig::default();
2584 assert_eq!(cfg.user_agent, "heardright/1.0");
2585 assert!(matches!(cfg.proxy, ProxyMode::Environment));
2586 assert_eq!(cfg.trust, TrustRoots::Bundled);
2587 assert_eq!(cfg.max_redirects, 10);
2588 assert_eq!(cfg.retry, RetryPolicy::none());
2589 assert_eq!(
2591 format!("{:?}", cfg.address_policy),
2592 format!("{:?}", base.address_policy)
2593 );
2594 assert_eq!(cfg.connect_timeout, base.connect_timeout);
2596 assert_eq!(cfg.request_timeout, base.request_timeout);
2597 assert_eq!(cfg.response_timeout, base.response_timeout);
2598 assert_eq!(cfg.recv_body_timeout, base.recv_body_timeout);
2599 assert_eq!(cfg.stream_timeout, base.stream_timeout);
2600 assert_eq!(cfg.idle_timeout, base.idle_timeout);
2601 assert_eq!(cfg.max_redirects_will_error, base.max_redirects_will_error);
2602 assert_eq!(cfg.max_response_bytes, base.max_response_bytes);
2603 assert!(cfg.on_retry.is_none());
2604 assert!(cfg.on_attempt.is_none());
2605 assert!(cfg.redact_values.is_empty());
2606 assert_eq!(cfg.proxy_bypass_first_party, base.proxy_bypass_first_party);
2607 assert_eq!(cfg.https_only, base.https_only);
2608 }
2609
2610 #[test]
2611 fn put_and_delete_set_their_methods() {
2612 assert_eq!(Request::put("http://x/").method, Method::Put);
2613 assert_eq!(Request::delete("http://x/").method, Method::Delete);
2614 assert!(Request::put("http://x/").body.is_empty());
2615 }
2616
2617 #[test]
2618 fn set_header_replaces_case_insensitively_in_place() {
2619 let req = Request::post("http://x/")
2620 .header("X-A", "1")
2621 .header("Accept", "text/plain")
2622 .header("x-a", "2")
2623 .set_header("X-A", "3");
2624 assert_eq!(
2625 req.headers,
2626 vec![
2627 ("X-A".to_string(), "3".to_string()),
2628 ("Accept".to_string(), "text/plain".to_string()),
2629 ]
2630 );
2631 let req = req.set_header("content-type", "text/csv");
2632 assert_eq!(
2633 req.headers.last(),
2634 Some(&("content-type".to_string(), "text/csv".to_string()))
2635 );
2636 }
2637
2638 #[test]
2639 fn redaction_replaces_values_and_longest_first_scrub() {
2640 let values = vec!["secret-token-123".to_string(), "secret".to_string()];
2641 assert_eq!(
2642 redact_str("bad secret-token-123 and secret", &values),
2643 "bad [redacted] and [redacted]"
2644 );
2645 }
2646
2647 #[test]
2648 fn secret_header_hidden_in_debug_but_sent() {
2649 let req = Request::get("http://x/").secret_header("X-Key", "hunter2-value");
2650 assert!(req.headers.iter().any(|(_, v)| v == "hunter2-value"));
2651 let debug = format!("{req:?}");
2652 assert!(!debug.contains("hunter2-value"), "{debug}");
2653 assert!(debug.contains(REDACTED));
2654 }
2655
2656 #[test]
2657 fn scrub_error_covers_text_variants() {
2658 let values = vec!["tok-9".to_string()];
2659 match scrub_error(
2660 HttpError::Status {
2661 status: 401,
2662 body: "invalid tok-9".into(),
2663 retry_after: None,
2664 attempts: 1,
2665 },
2666 &values,
2667 ) {
2668 HttpError::Status { body, .. } => assert_eq!(body, "invalid [redacted]"),
2669 other => panic!("unexpected {other:?}"),
2670 }
2671 }
2672
2673 fn env_of(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<String> {
2674 move |name| {
2675 pairs
2676 .iter()
2677 .find(|(k, _)| *k == name)
2678 .map(|(_, v)| v.to_string())
2679 }
2680 }
2681
2682 fn uri(s: &str) -> ureq::http::Uri {
2683 s.parse().unwrap()
2684 }
2685
2686 #[test]
2687 fn environment_proxy_uses_the_first_set_variable_and_skips_empty_ones() {
2688 let env = env_of(&[
2689 ("ALL_PROXY", ""),
2690 ("HTTPS_PROXY", "http://secure.proxy:3128"),
2691 ("HTTP_PROXY", "http://plain.proxy:8080"),
2692 ("NO_PROXY", "a.test, .b.test ,"),
2693 ]);
2694 let p = proxy_from_lookup(&env).unwrap().unwrap();
2695 assert_eq!(p.host, "secure.proxy");
2696 assert_eq!(p.port, 3128);
2697 assert_eq!(
2698 p.no_proxy,
2699 vec!["a.test".to_string(), ".b.test".to_string()]
2700 );
2701 assert!(p.ureq.is_no_proxy(&uri("http://a.test/")));
2702 assert!(p.ureq.is_no_proxy(&uri("http://x.b.test/")));
2703 assert!(!p.ureq.is_no_proxy(&uri("http://c.test/")));
2704 }
2705
2706 #[test]
2707 fn environment_proxy_order_and_case() {
2708 let env = env_of(&[
2709 ("http_proxy", "http://lower.proxy:1"),
2710 ("ALL_PROXY", "http://all.proxy:2"),
2711 ]);
2712 assert_eq!(proxy_from_lookup(&env).unwrap().unwrap().host, "all.proxy");
2713 let env = env_of(&[("http_proxy", "http://lower.proxy:1")]);
2714 assert_eq!(
2715 proxy_from_lookup(&env).unwrap().unwrap().host,
2716 "lower.proxy"
2717 );
2718 let env = env_of(&[("no_proxy", "z.test"), ("https_proxy", "http://p.proxy:9")]);
2719 assert_eq!(
2720 proxy_from_lookup(&env).unwrap().unwrap().no_proxy,
2721 vec!["z.test".to_string()]
2722 );
2723 }
2724
2725 #[test]
2726 fn environment_without_a_usable_proxy_is_none() {
2727 assert!(proxy_from_lookup(&env_of(&[])).unwrap().is_none());
2728 let env = env_of(&[
2730 ("ALL_PROXY", "ftp://nope.proxy:21"),
2731 ("HTTP_PROXY", "http://ok.proxy:80"),
2732 ]);
2733 assert_eq!(proxy_from_lookup(&env).unwrap().unwrap().host, "ok.proxy");
2734 assert!(
2735 proxy_from_lookup(&env_of(&[("ALL_PROXY", "ftp://nope.proxy:21")]))
2736 .unwrap()
2737 .is_none()
2738 );
2739 }
2740
2741 #[test]
2742 fn explicit_proxy_lets_the_proxy_resolve_and_keeps_credentials() {
2743 let p = build_proxy("http://user:pass@Proxy.Corp:3128", &[]).unwrap();
2744 assert_eq!(p.host, "proxy.corp");
2745 assert_eq!(p.port, 3128);
2746 assert!(!p.ureq.resolve_target());
2747 assert_eq!(p.ureq.username(), Some("user"));
2748 assert_eq!(p.ureq.password(), Some("pass"));
2749 assert!(build_proxy("https://10.0.0.5:3128", &[]).is_ok());
2750 assert!(build_proxy("socks5://127.0.0.1:1080", &[]).is_err());
2751 assert!(build_proxy("ftp://x:1", &[]).is_err());
2752 assert!(build_proxy("not a url", &[]).is_err());
2753 }
2754
2755 #[test]
2756 fn proxy_mode_debug_never_shows_credentials() {
2757 let shown = format!(
2758 "{:?}",
2759 ProxyMode::Explicit("http://user:pw-9@proxy:3128/x?k=v".into())
2760 );
2761 assert!(
2762 !shown.contains("pw-9") && !shown.contains("user:") && !shown.contains("k=v"),
2763 "{shown}"
2764 );
2765 assert_eq!(format!("{:?}", ProxyMode::None), "None");
2766 assert_eq!(format!("{:?}", ProxyMode::Environment), "Environment");
2767 assert_eq!(format!("{:?}", ProxyMode::System), "System");
2768 }
2769
2770 #[test]
2771 fn system_settings_resolve_like_an_environment_proxy() {
2772 use crate::system_proxy::SystemProxySettings;
2773 let proxy = proxy_from_system(&SystemProxySettings {
2774 proxy: Some("http=web.corp:8080;https=web.corp:8080".into()),
2775 bypass: Some("*.corp.example;intranet".into()),
2776 ..SystemProxySettings::default()
2777 })
2778 .unwrap()
2779 .expect("a static system proxy resolves");
2780 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
2781 assert_eq!(
2782 proxy.no_proxy,
2783 vec![".corp.example".to_string(), "intranet".to_string()]
2784 );
2785 assert!(ProxyDiagnostic::of(&proxy).from_environment);
2786 assert!(proxy_from_system(&SystemProxySettings::default())
2788 .unwrap()
2789 .is_none());
2790 }
2791
2792 #[test]
2793 fn system_settings_a_pac_script_fails_closed_and_split_configs_need_per_request_routing() {
2794 use crate::system_proxy::SystemProxySettings;
2795 let pac = SystemProxySettings {
2797 auto_config: true,
2798 ..SystemProxySettings::default()
2799 };
2800 let expected = "a proxy auto-config script is set";
2801 let error = proxy_from_system(&pac)
2802 .err()
2803 .expect("a PAC script is refused");
2804 assert!(
2805 matches!(&error, HttpError::SystemProxyUnsupported { element } if element == expected),
2806 "{error}"
2807 );
2808 assert_eq!(
2809 error.to_string(),
2810 format!(
2811 "the Windows proxy settings cannot be followed exactly by this version \
2812 ({expected}); nothing was sent"
2813 )
2814 );
2815 assert!(matches!(error.class(), crate::error::ErrorClass::Invalid));
2816 for (proxy, bypass) in [
2819 ("http=web.corp:8080;https=secure.corp:8443", None),
2820 ("https=secure.corp:8443", None),
2821 ("web.corp:8080", Some("<local>")),
2822 ("web.corp:8080", Some("https://only.example")),
2823 ] {
2824 let settings = SystemProxySettings {
2825 proxy: Some(proxy.into()),
2826 bypass: bypass.map(str::to_string),
2827 ..SystemProxySettings::default()
2828 };
2829 let no_env = |_: &str| None::<String>;
2830 let (routing, used) = resolve_system_routing(&settings, &no_env).unwrap();
2831 assert!(
2832 matches!(routing, Routing::PerRequest(_)),
2833 "{proxy} {bypass:?}"
2834 );
2835 assert_eq!(used, ProxyModeUsed::System);
2836 }
2837 }
2838
2839 #[test]
2840 fn system_mode_with_automatic_detection_only_goes_direct_or_environment() {
2841 use crate::system_proxy::SystemProxySettings;
2842 let no_env = |_: &str| None::<String>;
2843 let env_proxy =
2844 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2845 let detect = SystemProxySettings {
2847 auto_detect: true,
2848 ..SystemProxySettings::default()
2849 };
2850 let reason = "automatic detection only; no static proxy or PAC configured";
2851 let (proxy, used) = resolve_system(&detect, &no_env).unwrap();
2852 assert!(proxy.is_none());
2853 assert_eq!(
2854 used,
2855 ProxyModeUsed::SystemFallbackDirect {
2856 reason: reason.to_string()
2857 }
2858 );
2859 let (proxy, used) = resolve_system(&detect, &env_proxy).unwrap();
2860 let proxy = proxy.expect("the environment proxy is used");
2861 assert_eq!((proxy.host.as_str(), proxy.port), ("env.corp", 3128));
2862 assert!(proxy.from_env);
2863 assert_eq!(
2864 used,
2865 ProxyModeUsed::SystemFallbackEnvironment {
2866 reason: reason.to_string()
2867 }
2868 );
2869 let with_bypass = SystemProxySettings {
2871 bypass: Some("<local>".into()),
2872 ..detect
2873 };
2874 let (proxy, used) = resolve_system(&with_bypass, &no_env).unwrap();
2875 assert!(proxy.is_none());
2876 assert!(matches!(used, ProxyModeUsed::SystemFallbackDirect { .. }));
2877 }
2878
2879 #[test]
2880 fn system_mode_with_automatic_detection_and_a_static_proxy_follows_the_proxy() {
2881 use crate::system_proxy::SystemProxySettings;
2882 let no_env = |_: &str| None::<String>;
2883 let env_proxy =
2884 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2885 let settings = SystemProxySettings {
2886 proxy: Some("web.corp:8080".into()),
2887 bypass: Some("localhost".into()),
2888 auto_detect: true,
2889 ..SystemProxySettings::default()
2890 };
2891 for get in [&no_env as &dyn Fn(&str) -> Option<String>, &env_proxy] {
2892 let (proxy, used) = resolve_system(&settings, get).unwrap();
2893 let proxy = proxy.expect("the static proxy is used");
2894 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
2895 assert_eq!(used, ProxyModeUsed::System);
2896 }
2897 }
2898
2899 #[test]
2900 fn system_mode_still_refuses_a_pac_script() {
2901 use crate::system_proxy::SystemProxySettings;
2902 let no_env = |_: &str| None::<String>;
2903 let env_proxy =
2904 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
2905 let static_proxy = |proxy: &str, bypass: Option<&str>| SystemProxySettings {
2906 proxy: Some(proxy.into()),
2907 bypass: bypass.map(str::to_string),
2908 ..SystemProxySettings::default()
2909 };
2910 let rows: Vec<(SystemProxySettings, &str)> = vec![
2911 (
2912 SystemProxySettings {
2913 auto_config: true,
2914 ..SystemProxySettings::default()
2915 },
2916 "a proxy auto-config script is set",
2917 ),
2918 (
2919 SystemProxySettings {
2920 auto_config: true,
2921 auto_detect: true,
2922 ..static_proxy("web.corp:8080", None)
2923 },
2924 "a proxy auto-config script is set",
2925 ),
2926 ];
2927 for (settings, reason) in rows {
2930 for get in [&no_env as &dyn Fn(&str) -> Option<String>, &env_proxy] {
2931 let error = resolve_system(&settings, get)
2932 .err()
2933 .unwrap_or_else(|| panic!("{reason}: must be refused"));
2934 assert!(
2935 matches!(&error, HttpError::SystemProxyUnsupported { element } if element == reason),
2936 "{reason}: {error}"
2937 );
2938 }
2939 }
2940 }
2941
2942 #[test]
2943 fn system_mode_reports_what_it_followed() {
2944 use crate::system_proxy::SystemProxySettings;
2945 let no_env = |_: &str| None::<String>;
2946 let env_proxy =
2947 |name: &str| (name == "HTTP_PROXY").then(|| "http://env.corp:3128".to_string());
2948 let followed = SystemProxySettings {
2950 proxy: Some("web.corp:8080".into()),
2951 bypass: Some("localhost;*.corp.example".into()),
2952 ..SystemProxySettings::default()
2953 };
2954 let (proxy, used) = resolve_system(&followed, &no_env).unwrap();
2955 let proxy = proxy.expect("the system proxy is used");
2956 assert_eq!((proxy.host.as_str(), proxy.port), ("web.corp", 8080));
2957 assert_eq!(used, ProxyModeUsed::System);
2958 let (proxy, used) = resolve_system(&followed, &env_proxy).unwrap();
2959 assert_eq!(proxy.expect("the system proxy wins").host, "web.corp");
2960 assert_eq!(used, ProxyModeUsed::System);
2961 let none = SystemProxySettings::default();
2964 let (proxy, used) = resolve_system(&none, &no_env).unwrap();
2965 assert!(proxy.is_none());
2966 assert_eq!(used, ProxyModeUsed::Direct);
2967 let (proxy, used) = resolve_system(&none, &env_proxy).unwrap();
2968 assert!(proxy.is_some());
2969 assert_eq!(used, ProxyModeUsed::Environment);
2970 }
2971
2972 #[test]
2973 fn system_for_url_follows_the_resolver_then_the_static_settings() {
2974 use crate::system_proxy::{SystemProxySettings, UrlRoute};
2975 let no_env = |_: &str| None::<String>;
2976 let url = "https://updates.example.com/feed.json";
2977 let never = |_: &str, _: &SystemProxySettings| None::<UrlRoute>;
2978 let pac_says =
2979 |route: UrlRoute| move |_: &str, _: &SystemProxySettings| Some(route.clone());
2980
2981 let pac = SystemProxySettings {
2983 auto_config: true,
2984 auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
2985 proxy: Some("static.corp:8080".into()),
2986 ..SystemProxySettings::default()
2987 };
2988 let os = pac_says(UrlRoute::Proxy("http://pac.corp:3128".into()));
2989 let (proxy, used) = resolve_system_for_url(&pac, url, &os, &no_env).unwrap();
2990 assert_eq!(proxy.expect("PAC proxy").host, "pac.corp");
2991 assert_eq!(used, ProxyModeUsed::SystemForUrl);
2992 let os = pac_says(UrlRoute::Direct);
2993 let (proxy, used) = resolve_system_for_url(&pac, url, &os, &no_env).unwrap();
2994 assert!(proxy.is_none());
2995 assert_eq!(used, ProxyModeUsed::SystemForUrl);
2996
2997 let err = resolve_system_for_url(&pac, url, &never, &no_env)
2999 .err()
3000 .unwrap();
3001 assert!(
3002 matches!(err, HttpError::SystemProxyUnsupported { .. }),
3003 "{err:?}"
3004 );
3005
3006 let detect = SystemProxySettings {
3008 auto_detect: true,
3009 proxy: Some("http=web.corp:8080;https=secure.corp:8443".into()),
3010 bypass: Some("<local>;*.corp.example;http://plain.example".into()),
3011 ..SystemProxySettings::default()
3012 };
3013 let routed = |target: &str| {
3014 resolve_system_for_url(&detect, target, &never, &no_env)
3015 .unwrap()
3016 .0
3017 .map(|p| p.host)
3018 };
3019 assert_eq!(
3020 routed("https://api.example.com/").as_deref(),
3021 Some("secure.corp")
3022 );
3023 assert_eq!(
3024 routed("http://api.example.com/").as_deref(),
3025 Some("web.corp")
3026 );
3027 assert_eq!(routed("https://intranet/").as_deref(), None);
3028 assert_eq!(routed("https://a.corp.example/").as_deref(), None);
3029 assert_eq!(routed("http://plain.example/").as_deref(), None);
3030 assert_eq!(
3031 routed("https://plain.example/").as_deref(),
3032 Some("secure.corp")
3033 );
3034
3035 let only = SystemProxySettings {
3037 auto_detect: true,
3038 ..SystemProxySettings::default()
3039 };
3040 let (proxy, used) = resolve_system_for_url(&only, url, &never, &no_env).unwrap();
3041 assert!(proxy.is_none());
3042 assert!(matches!(used, ProxyModeUsed::SystemFallbackDirect { .. }));
3043 let env_proxy =
3044 |name: &str| (name == "HTTPS_PROXY").then(|| "http://env.corp:3128".to_string());
3045 let (proxy, used) = resolve_system_for_url(&only, url, &never, &env_proxy).unwrap();
3046 assert_eq!(proxy.expect("environment proxy").host, "env.corp");
3047 assert!(matches!(
3048 used,
3049 ProxyModeUsed::SystemFallbackEnvironment { .. }
3050 ));
3051
3052 for bad in ["not a url", "ftp://host/file", "/relative"] {
3054 let err = resolve_system_for_url(&only, bad, &never, &no_env)
3055 .err()
3056 .unwrap();
3057 assert!(
3058 matches!(err, HttpError::InvalidRequest(_)),
3059 "{bad}: {err:?}"
3060 );
3061 }
3062 }
3063
3064 #[test]
3065 fn system_for_url_debug_hides_credentials() {
3066 let shown = format!(
3067 "{:?}",
3068 ProxyMode::SystemForUrl("https://u:pw-9@host/p?k=v".into())
3069 );
3070 assert!(!shown.contains("pw-9") && !shown.contains("k=v"), "{shown}");
3071 }
3072
3073 #[test]
3074 fn first_party_bypass_drops_the_proxy_only_when_asked() {
3075 let base = ClientConfig::first_party(vec!["api.example.com".into()])
3076 .proxy(ProxyMode::Explicit("http://proxy:3128".into()));
3077 assert!(base.resolved_proxy().unwrap().is_some());
3078 assert!(base
3079 .clone()
3080 .proxy_bypass_first_party(true)
3081 .resolved_proxy()
3082 .unwrap()
3083 .is_none());
3084 let public = ClientConfig::default()
3086 .proxy(ProxyMode::Explicit("http://proxy:3128".into()))
3087 .proxy_bypass_first_party(true);
3088 assert!(public.resolved_proxy().unwrap().is_some());
3089 assert!(ClientConfig::default().resolved_proxy().unwrap().is_none());
3090 }
3091
3092 #[test]
3093 fn invalid_explicit_proxy_fails_closed() {
3094 let config = ClientConfig::default().proxy(ProxyMode::Explicit("ftp://nope:1".into()));
3095 assert!(matches!(
3096 Client::try_new(config.clone()),
3097 Err(HttpError::InvalidRequest(_))
3098 ));
3099 let err = Client::new(config)
3100 .send(&Request::get("http://127.0.0.1:9/"))
3101 .unwrap_err();
3102 assert!(matches!(err, HttpError::InvalidRequest(_)), "{err:?}");
3103 }
3104
3105 fn split_settings() -> crate::system_proxy::SystemProxySettings {
3108 crate::system_proxy::SystemProxySettings {
3109 proxy: Some("http=plain.corp:8080;https=secure.corp:8443".into()),
3110 bypass: Some("<local>;*.corp.example;https://only-tls.example".into()),
3111 ..crate::system_proxy::SystemProxySettings::default()
3112 }
3113 }
3114
3115 #[test]
3116 fn per_scheme_proxies_are_chosen_per_request_url() {
3117 let routes = SystemRoutes::new(&split_settings()).unwrap();
3118 let at = |scheme: &str, host: &str| {
3119 routes
3120 .proxy_for(scheme, host)
3121 .map(|p| (p.host.clone(), p.port))
3122 };
3123 assert_eq!(
3124 at("http", "api.example.com"),
3125 Some(("plain.corp".into(), 8080))
3126 );
3127 assert_eq!(
3128 at("https", "api.example.com"),
3129 Some(("secure.corp".into(), 8443))
3130 );
3131 let mut endpoints = routes.endpoints();
3132 endpoints.sort();
3133 assert_eq!(
3134 endpoints,
3135 vec![
3136 ("plain.corp".to_string(), 8080),
3137 ("secure.corp".to_string(), 8443)
3138 ]
3139 );
3140 }
3141
3142 #[test]
3143 fn a_scheme_limited_bypass_entry_bypasses_that_scheme_only() {
3144 let routes = SystemRoutes::new(&split_settings()).unwrap();
3145 assert!(routes.proxy_for("https", "only-tls.example").is_none());
3146 assert_eq!(
3147 routes.proxy_for("http", "only-tls.example").map(|p| p.port),
3148 Some(8080)
3149 );
3150 }
3151
3152 #[test]
3153 fn local_and_wildcard_bypass_entries_go_direct_on_both_schemes() {
3154 let routes = SystemRoutes::new(&split_settings()).unwrap();
3155 for scheme in ["http", "https"] {
3156 assert!(
3157 routes.proxy_for(scheme, "intranet").is_none(),
3158 "{scheme} <local>"
3159 );
3160 assert!(
3161 routes.proxy_for(scheme, "a.corp.example").is_none(),
3162 "{scheme} *.corp"
3163 );
3164 assert!(
3165 routes.proxy_for(scheme, "intranet.other.com").is_some(),
3166 "{scheme}"
3167 );
3168 }
3169 }
3170
3171 #[test]
3172 fn a_scheme_without_an_endpoint_is_direct() {
3173 let settings = crate::system_proxy::SystemProxySettings {
3174 proxy: Some("https=secure.corp:8443".into()),
3175 ..crate::system_proxy::SystemProxySettings::default()
3176 };
3177 let routes = SystemRoutes::new(&settings).unwrap();
3178 assert!(routes.proxy_for("http", "x.example.com").is_none());
3179 assert!(routes.proxy_for("https", "x.example.com").is_some());
3180 }
3181
3182 fn recording_proxy() -> (u16, std::sync::Arc<std::sync::Mutex<Vec<String>>>) {
3184 use std::io::{BufRead, BufReader, Write};
3185 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
3186 let port = listener.local_addr().unwrap().port();
3187 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3188 let log = seen.clone();
3189 std::thread::spawn(move || {
3190 for stream in listener.incoming() {
3191 let Ok(mut stream) = stream else { break };
3192 let mut first = String::new();
3193 let mut reader = BufReader::new(stream.try_clone().unwrap());
3194 let _ = reader.read_line(&mut first);
3195 log.lock().unwrap().push(first.trim().to_string());
3196 let _ = stream.write_all(
3197 b"HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n",
3198 );
3199 }
3200 });
3201 (port, seen)
3202 }
3203
3204 #[test]
3205 fn the_blocking_client_sends_each_request_to_its_own_scheme_proxy() {
3206 let (http_port, http_seen) = recording_proxy();
3207 let (https_port, https_seen) = recording_proxy();
3208 let settings = crate::system_proxy::SystemProxySettings {
3209 proxy: Some(format!(
3210 "http=127.0.0.1:{http_port};https=127.0.0.1:{https_port}"
3211 )),
3212 bypass: Some("<local>;https://tls-only.example".into()),
3213 ..crate::system_proxy::SystemProxySettings::default()
3214 };
3215 let config = ClientConfig {
3216 address_policy: AddressPolicy::default().with_resolver(|_| vec![]),
3217 retry: RetryPolicy::none(),
3218 request_timeout: Duration::from_secs(5),
3219 response_timeout: Some(Duration::from_secs(5)),
3220 connect_timeout: Some(Duration::from_secs(3)),
3221 ..ClientConfig::default()
3222 };
3223 let client = Client::build(
3224 config,
3225 Routing::PerRequest(Arc::new(SystemRoutes::new(&settings).unwrap())),
3226 ProxyModeUsed::System,
3227 );
3228 assert!(client.proxy_diagnostic().is_none());
3229 let go = |url: &str| {
3230 let _ = client.send(&Request::get(url));
3231 };
3232 go("http://app.example.com/");
3233 go("https://app.example.com/");
3234 go("http://tls-only.example/");
3235 go("https://tls-only.example/");
3237 go("http://intranet/");
3238 let http_seen = http_seen.lock().unwrap().clone();
3239 let https_seen = https_seen.lock().unwrap().clone();
3240 assert_eq!(
3241 http_seen,
3242 vec![
3243 "CONNECT app.example.com:80 HTTP/1.1".to_string(),
3244 "CONNECT tls-only.example:80 HTTP/1.1".to_string(),
3245 ]
3246 );
3247 assert_eq!(
3248 https_seen,
3249 vec!["CONNECT app.example.com:443 HTTP/1.1".to_string()]
3250 );
3251 }
3252
3253 #[test]
3254 fn component_encoding_and_repeated_headers() {
3255 assert_eq!(percent_encode_component("a b/c+~"), "a%20b%2Fc%2B~");
3256 assert_eq!(percent_decode_component("a%20b%2Fc+d%zz"), "a b/c+d%zz");
3257 let r = Response {
3258 status: 200,
3259 headers: vec![
3260 ("set-cookie".into(), "a=1".into()),
3261 ("set-cookie".into(), "b=2".into()),
3262 ],
3263 body: Vec::new(),
3264 attempts: 1,
3265 final_url: "https://x.test/".into(),
3266 redact: Vec::new(),
3267 };
3268 assert_eq!(
3269 r.header_all("Set-Cookie").collect::<Vec<_>>(),
3270 vec!["a=1", "b=2"]
3271 );
3272 assert_eq!(r.header("set-cookie"), Some("a=1"));
3273 assert_eq!(r.final_url(), "https://x.test/");
3274 }
3275}