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