1use crate::net::cors::{self, CorsError, ResponseTainting};
4use crate::net::fetch_metadata::{RequestDestination, RequestMode};
5use crate::net::mixed_content::{is_origin_potentially_trustworthy, MixedContentPolicy};
6use crate::net::referrer::{self, ReferrerPolicy};
7use crate::net::request_ref::RequestReference;
8use crate::net::shared_body::SharedBody;
9use crate::net::tls::TlsError;
10use crate::net::utils::{normalize_url, short_hash, BytesAsyncReader};
11use crate::types::{PeekBuf, RequestId};
12use bytes::Bytes;
13use http::{header, HeaderMap, Method};
14use std::fmt::{Debug, Display};
15use std::hash::Hash;
16use std::pin::Pin;
17use std::sync::Arc;
18use tokio::io::{AsyncRead, ReadBuf};
19use url::{Origin, Url};
20
21#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
24pub enum Priority {
25 High,
27 #[default]
29 Normal,
30 Low,
32 Idle,
34}
35
36impl Display for Priority {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 let s = match self {
39 Priority::High => "High",
40 Priority::Normal => "Normal",
41 Priority::Low => "Low",
42 Priority::Idle => "Idle",
43 };
44 f.write_str(s)
45 }
46}
47
48#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
54pub enum ResourceKind {
55 #[default]
57 Primary,
58 Asset,
60 Other,
62}
63
64#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
71pub enum Initiator {
72 #[default]
74 User,
75 Application,
77 Other,
79}
80
81#[derive(Clone, Debug)]
83pub struct FetchResultMeta {
84 pub final_url: Url,
86 pub status: u16,
88 pub status_text: String,
90 pub headers: HeaderMap,
92 pub content_length: Option<u64>,
94 pub content_type: Option<String>,
96 pub has_body: bool,
98 pub tainting: ResponseTainting,
105}
106
107impl FetchResultMeta {
108 pub fn readable_headers(&self, credentials_include: bool) -> HeaderMap {
117 cors::readable_headers(self.tainting, &self.headers, credentials_include)
118 }
119}
120
121#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
127pub enum BlockReason {
128 MixedContent,
131 UrlPolicy,
133 UnsupportedScheme,
135 Cors(CorsError),
138}
139
140impl Display for BlockReason {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let s = match self {
143 BlockReason::MixedContent => "mixed content",
144 BlockReason::UrlPolicy => "blocked by URL policy",
145 BlockReason::UnsupportedScheme => "unsupported URL scheme",
146 BlockReason::Cors(err) => return write!(f, "CORS: {err}"),
147 };
148 f.write_str(s)
149 }
150}
151
152#[derive(Debug, thiserror::Error, Clone)]
154pub enum NetError {
155 #[error("net error: blocked: {reason}: {url}")]
157 Blocked {
158 reason: BlockReason,
160 url: Url,
163 },
164
165 #[error("net error: reqwest: {0}")]
167 Reqwest(#[from] Arc<reqwest::Error>),
168
169 #[error("net error: tls: {0}")]
172 Tls(TlsError),
173
174 #[error("net error: redirect: {0}")]
176 Redirect(Arc<anyhow::Error>),
177
178 #[error("net error: I/O: {0}")]
180 Io(#[from] Arc<std::io::Error>),
181
182 #[error("net error: cancelled: {0}")]
184 Cancelled(String),
185
186 #[error(transparent)]
188 Read(Arc<anyhow::Error>),
189
190 #[error(transparent)]
192 Other(Arc<anyhow::Error>),
193
194 #[error("net error: timeout: {0}")]
196 Timeout(String),
197}
198
199impl From<std::io::Error> for NetError {
200 fn from(e: std::io::Error) -> Self {
201 NetError::Io(Arc::new(e))
202 }
203}
204
205impl NetError {
206 pub fn to_io(&self) -> std::io::Error {
210 std::io::Error::other(self.clone())
211 }
212
213 pub fn from_anyhow(e: anyhow::Error) -> Self {
215 Self::Read(Arc::new(e))
216 }
217}
218
219#[cfg(not(target_arch = "wasm32"))]
222pub trait MaybeSend: Send {}
223#[cfg(not(target_arch = "wasm32"))]
224impl<T: Send> MaybeSend for T {}
225#[cfg(target_arch = "wasm32")]
228pub trait MaybeSend {}
229#[cfg(target_arch = "wasm32")]
230impl<T> MaybeSend for T {}
231
232#[cfg(not(target_arch = "wasm32"))]
235pub type BoxedAsyncRead = Pin<Box<dyn AsyncRead + Send + 'static>>;
236#[cfg(target_arch = "wasm32")]
239pub type BoxedAsyncRead = Pin<Box<dyn AsyncRead + 'static>>;
240
241pub struct BodyStream {
243 inner: BoxedAsyncRead,
245 pub len: Option<u64>,
247 pub is_seekable: bool,
249 pub clonable: bool,
251}
252
253impl Debug for BodyStream {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("BodyStream")
256 .field("len", &self.len)
257 .field("is_seekable", &self.is_seekable)
258 .field("clonable", &self.clonable)
259 .finish()
260 }
261}
262
263impl BodyStream {
264 pub fn new(inner: BoxedAsyncRead, len: Option<u64>) -> Self {
266 Self {
267 inner,
268 len,
269 is_seekable: false,
270 clonable: false,
271 }
272 }
273
274 pub fn from_bytes(bytes: Bytes) -> Self {
276 let len = bytes.len() as u64;
277 let reader = Box::pin(BytesAsyncReader {
278 data: bytes,
279 pos: 0,
280 });
281 Self {
282 inner: reader,
283 len: Some(len),
284 is_seekable: true, clonable: true, }
287 }
288}
289
290impl AsyncRead for BodyStream {
291 fn poll_read(
292 mut self: Pin<&mut Self>,
293 cx: &mut std::task::Context<'_>,
294 buf: &mut ReadBuf<'_>,
295 ) -> std::task::Poll<std::io::Result<()>> {
296 self.inner.as_mut().poll_read(cx, buf)
297 }
298}
299
300#[cfg(not(target_arch = "wasm32"))]
303pub type BodyStreamFactory =
304 Arc<dyn Fn() -> std::io::Result<BoxedAsyncRead> + Send + Sync + 'static>;
305
306#[derive(Clone, Default)]
313pub struct RequestBody {
314 payload: Payload,
315 pub content_type: Option<String>,
318}
319
320#[derive(Clone)]
321enum Payload {
322 Bytes(Bytes),
323 #[cfg(not(target_arch = "wasm32"))]
324 Stream {
325 open: BodyStreamFactory,
326 len: Option<u64>,
327 },
328}
329
330impl Default for Payload {
331 fn default() -> Self {
332 Payload::Bytes(Bytes::new())
333 }
334}
335
336impl Debug for RequestBody {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 let mut d = f.debug_struct("RequestBody");
339 match &self.payload {
340 Payload::Bytes(b) => d.field("bytes", &b.len()),
341 #[cfg(not(target_arch = "wasm32"))]
342 Payload::Stream { len, .. } => d.field("stream", len),
343 };
344 d.field("content_type", &self.content_type).finish()
345 }
346}
347
348impl RequestBody {
349 pub fn bytes(b: impl Into<Bytes>) -> Self {
351 Self {
352 payload: Payload::Bytes(b.into()),
353 content_type: None,
354 }
355 }
356
357 pub fn json(b: impl Into<Bytes>) -> Self {
359 Self {
360 content_type: Some("application/json".into()),
361 ..Self::bytes(b)
362 }
363 }
364
365 pub fn form(b: impl Into<Bytes>) -> Self {
367 Self {
368 content_type: Some("application/x-www-form-urlencoded".into()),
369 ..Self::bytes(b)
370 }
371 }
372
373 pub fn text(s: impl Into<String>) -> Self {
375 Self {
376 content_type: Some("text/plain; charset=utf-8".into()),
377 ..Self::bytes(s.into().into_bytes())
378 }
379 }
380
381 #[cfg(not(target_arch = "wasm32"))]
386 pub fn stream(
387 open: impl Fn() -> std::io::Result<BoxedAsyncRead> + Send + Sync + 'static,
388 len: Option<u64>,
389 ) -> Self {
390 Self {
391 payload: Payload::Stream {
392 open: Arc::new(open),
393 len,
394 },
395 content_type: None,
396 }
397 }
398
399 #[cfg(not(target_arch = "wasm32"))]
404 pub fn file(path: impl Into<std::path::PathBuf>) -> std::io::Result<Self> {
405 let path = path.into();
406 let len = std::fs::metadata(&path)?.len();
407 Ok(Self::stream(
408 move || {
409 let f = std::fs::File::open(&path)?;
410 Ok(Box::pin(tokio::fs::File::from_std(f)) as BoxedAsyncRead)
411 },
412 Some(len),
413 ))
414 }
415
416 pub fn as_bytes(&self) -> Option<&Bytes> {
418 match &self.payload {
419 Payload::Bytes(b) => Some(b),
420 #[cfg(not(target_arch = "wasm32"))]
421 Payload::Stream { .. } => None,
422 }
423 }
424
425 pub fn len(&self) -> Option<u64> {
427 match &self.payload {
428 Payload::Bytes(b) => Some(b.len() as u64),
429 #[cfg(not(target_arch = "wasm32"))]
430 Payload::Stream { len, .. } => *len,
431 }
432 }
433
434 pub fn is_empty(&self) -> bool {
436 self.len() == Some(0)
437 }
438
439 pub(crate) fn to_reqwest_body(&self) -> std::io::Result<(reqwest::Body, Option<u64>)> {
442 match &self.payload {
443 Payload::Bytes(b) => Ok((reqwest::Body::from(b.clone()), None)),
444 #[cfg(not(target_arch = "wasm32"))]
445 Payload::Stream { open, len } => {
446 let reader = open()?;
447 let stream = tokio_util::io::ReaderStream::new(reader);
448 Ok((reqwest::Body::wrap_stream(stream), *len))
449 }
450 }
451 }
452}
453
454#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
466pub enum RequestCredentials {
467 Omit,
469 SameOrigin,
473 #[default]
477 Include,
478}
479
480#[derive(Debug, Clone)]
482pub struct FetchRequest {
483 pub reference: RequestReference,
485 pub req_id: RequestId,
487 pub priority: Priority,
489 pub initiator: Initiator,
491 pub kind: ResourceKind,
493 pub streaming: bool,
495 pub auto_decode: bool,
497 pub max_bytes: Option<usize>,
499 pub method: Method,
501 pub url: Url,
503 pub origin: Option<Origin>,
512 pub mixed_content: Option<MixedContentPolicy>,
515 pub referrer: Option<Url>,
520 pub referrer_policy: ReferrerPolicy,
522 pub destination: RequestDestination,
525 pub mode: RequestMode,
528 pub credentials: RequestCredentials,
531 pub headers: HeaderMap,
533 pub body: Option<RequestBody>,
536}
537
538impl FetchRequest {
539 pub fn builder(method: Method, url: Url) -> FetchRequestBuilder {
541 FetchRequestBuilder::new(method, url)
542 }
543
544 pub fn generate_request_key(&self) -> Option<String> {
546 match self.method {
547 Method::GET | Method::HEAD => {}
548 _ => return None,
549 }
550
551 let url = normalize_url(&self.url);
552 let h = &self.headers;
553
554 let range = h
555 .get(header::RANGE)
556 .and_then(|v| v.to_str().ok())
557 .unwrap_or("");
558 let accept = h
559 .get(header::ACCEPT)
560 .and_then(|v| v.to_str().ok())
561 .unwrap_or("");
562 let accept_enc = h
563 .get(header::ACCEPT_ENCODING)
564 .and_then(|v| v.to_str().ok())
565 .unwrap_or("");
566 let accept_lang = h
567 .get(header::ACCEPT_LANGUAGE)
568 .and_then(|v| v.to_str().ok())
569 .unwrap_or("");
570
571 let auth_hash = h
572 .get(header::AUTHORIZATION)
573 .map(|v| format!("{:x}", short_hash(v.as_bytes())))
574 .unwrap_or_default();
575 let cookie_hash = h
576 .get(header::COOKIE)
577 .map(|v| format!("{:x}", short_hash(v.as_bytes())))
578 .unwrap_or_default();
579
580 let mixed_content = if !self
589 .origin
590 .as_ref()
591 .is_some_and(is_origin_potentially_trustworthy)
592 {
593 "n"
596 } else {
597 match self.mixed_content {
602 None => "default",
603 Some(MixedContentPolicy::Allow) => "allow",
604 Some(MixedContentPolicy::Upgrade) => "upgrade",
605 Some(MixedContentPolicy::Block) => "block",
606 }
607 };
608
609 let referrer = match self.referrer.as_ref() {
616 Some(r) if !referrer::never_sends(r, self.referrer_policy) => {
617 let source = match self.referrer_policy {
621 ReferrerPolicy::Origin | ReferrerPolicy::StrictOrigin => {
622 r.origin().ascii_serialization()
623 }
624 _ => r.as_str().to_string(),
625 };
626 let policy = match self.referrer_policy {
629 ReferrerPolicy::NoReferrer => "no-referrer",
630 ReferrerPolicy::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
631 ReferrerPolicy::SameOrigin => "same-origin",
632 ReferrerPolicy::Origin => "origin",
633 ReferrerPolicy::StrictOrigin => "strict-origin",
634 ReferrerPolicy::OriginWhenCrossOrigin => "origin-when-cross-origin",
635 ReferrerPolicy::StrictOriginWhenCrossOrigin => {
636 "strict-origin-when-cross-origin"
637 }
638 ReferrerPolicy::UnsafeUrl => "unsafe-url",
639 };
640 format!("{:x}:{}", short_hash(source.as_bytes()), policy)
641 }
642 _ => match self.headers.get(header::REFERER) {
646 Some(manual) => format!("h{:x}", short_hash(manual.as_bytes())),
647 None => "n".to_string(),
648 },
649 };
650
651 let fetch_meta = {
656 let origin = match self.origin.as_ref() {
657 Some(o) => format!("{:x}", short_hash(o.ascii_serialization().as_bytes())),
658 None => match self.headers.get(header::ORIGIN) {
661 Some(manual) => format!("h{:x}", short_hash(manual.as_bytes())),
662 None => "n".to_string(),
663 },
664 };
665 let user = if self.mode == RequestMode::Navigate && self.initiator == Initiator::User {
666 "u"
667 } else {
668 "-"
669 };
670 format!(
671 "{}:{}:{}:{}",
672 self.destination.as_str(),
673 self.mode.as_str(),
674 origin,
675 user
676 )
677 };
678
679 let credentials = match self.credentials {
683 RequestCredentials::Omit => "omit",
684 RequestCredentials::SameOrigin => "same-origin",
685 RequestCredentials::Include => "include",
686 };
687
688 Some(format!(
689 "M={};U={};R={};A={};AL={};AE={};Auth={};C={};MC={};Ref={};FM={};Cred={}",
690 self.method,
691 url,
692 range,
693 accept,
694 accept_lang,
695 accept_enc,
696 auth_hash,
697 cookie_hash,
698 mixed_content,
699 referrer,
700 fetch_meta,
701 credentials
702 ))
703 }
704}
705
706pub struct FetchRequestBuilder {
711 reference: RequestReference,
712 req_id: RequestId,
713 priority: Priority,
714 initiator: Initiator,
715 kind: ResourceKind,
716 streaming: bool,
717 auto_decode: bool,
718 max_bytes: Option<usize>,
719 method: Method,
720 headers: HeaderMap,
721 url: Url,
722 origin: Option<Origin>,
723 mixed_content: Option<MixedContentPolicy>,
724 referrer: Option<Url>,
725 referrer_policy: ReferrerPolicy,
726 destination: RequestDestination,
727 mode: RequestMode,
728 credentials: RequestCredentials,
729 body: Option<RequestBody>,
730}
731
732impl FetchRequestBuilder {
733 pub fn new(method: Method, url: Url) -> Self {
735 Self {
736 url,
737 method,
738 headers: HeaderMap::default(),
739 reference: RequestReference::default(),
740 req_id: RequestId::default(),
741 priority: Priority::default(),
742 initiator: Initiator::default(),
743 kind: ResourceKind::default(),
744 streaming: false,
745 auto_decode: true,
746 max_bytes: None,
747 origin: None,
748 mixed_content: None,
749 referrer: None,
750 referrer_policy: ReferrerPolicy::default(),
751 destination: RequestDestination::default(),
752 mode: RequestMode::default(),
753 credentials: RequestCredentials::default(),
754 body: None,
755 }
756 }
757
758 pub fn with_reference(mut self, reference: RequestReference) -> Self {
760 self.reference = reference;
761 self
762 }
763
764 pub fn with_req_id(mut self, req_id: RequestId) -> Self {
766 self.req_id = req_id;
767 self
768 }
769
770 pub fn with_priority(mut self, priority: Priority) -> Self {
772 self.priority = priority;
773 self
774 }
775
776 pub fn with_initiator(mut self, initiator: Initiator) -> Self {
778 self.initiator = initiator;
779 self
780 }
781
782 pub fn with_kind(mut self, kind: ResourceKind) -> Self {
784 self.kind = kind;
785 self
786 }
787
788 pub fn with_streaming(mut self, streaming: bool) -> Self {
790 self.streaming = streaming;
791 self
792 }
793
794 pub fn with_auto_decode(mut self, auto_decode: bool) -> Self {
799 self.auto_decode = auto_decode;
800 self
801 }
802
803 pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
805 self.max_bytes = Some(max_bytes);
806 self
807 }
808
809 pub fn with_body(mut self, body: RequestBody) -> Self {
811 self.body = Some(body);
812 self
813 }
814
815 pub fn with_url(mut self, url: Url) -> Self {
817 self.url = url;
818 self
819 }
820
821 pub fn with_origin(mut self, origin: Origin) -> Self {
826 self.origin = Some(origin);
827 self
828 }
829
830 pub fn with_mixed_content(mut self, policy: MixedContentPolicy) -> Self {
833 self.mixed_content = Some(policy);
834 self
835 }
836
837 pub fn with_referrer(mut self, referrer: Url) -> Self {
840 self.referrer = Some(referrer);
841 self
842 }
843
844 pub fn with_referrer_policy(mut self, policy: ReferrerPolicy) -> Self {
846 self.referrer_policy = policy;
847 self
848 }
849
850 pub fn with_destination(mut self, destination: RequestDestination) -> Self {
853 self.destination = destination;
854 self
855 }
856
857 pub fn with_mode(mut self, mode: RequestMode) -> Self {
860 self.mode = mode;
861 self
862 }
863
864 pub fn with_credentials(mut self, credentials: RequestCredentials) -> Self {
867 self.credentials = credentials;
868 self
869 }
870
871 pub fn with_method(mut self, method: Method) -> Self {
873 self.method = method;
874 self
875 }
876
877 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
879 self.headers = headers;
880 self
881 }
882
883 pub fn build(self) -> FetchRequest {
885 FetchRequest {
886 reference: self.reference,
887 req_id: self.req_id,
888 priority: self.priority,
889 initiator: self.initiator,
890 kind: self.kind,
891 streaming: self.streaming,
892 auto_decode: self.auto_decode,
893 max_bytes: self.max_bytes,
894 headers: self.headers,
895 method: self.method,
896 url: self.url,
897 origin: self.origin,
898 mixed_content: self.mixed_content,
899 referrer: self.referrer,
900 referrer_policy: self.referrer_policy,
901 destination: self.destination,
902 mode: self.mode,
903 credentials: self.credentials,
904 body: self.body,
905 }
906 }
907}
908
909#[derive(Clone)]
911pub enum FetchResult {
912 Stream {
914 meta: FetchResultMeta,
916 peek_buf: PeekBuf,
918 shared: Arc<SharedBody>,
920 },
921 Buffered {
923 meta: FetchResultMeta,
925 body: Bytes,
927 },
928 Error(NetError),
930}
931
932impl FetchResult {
933 pub fn is_error(&self) -> bool {
935 matches!(self, FetchResult::Error(_))
936 }
937
938 pub fn meta(&self) -> Option<&FetchResultMeta> {
940 match self {
941 FetchResult::Stream { meta, .. } => Some(meta),
942 FetchResult::Buffered { meta, .. } => Some(meta),
943 FetchResult::Error(_) => None,
944 }
945 }
946}
947
948impl Debug for FetchResult {
949 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
950 match self {
951 FetchResult::Stream { meta, .. } => f
952 .debug_struct("FetchResult::Stream")
953 .field("meta", meta)
954 .finish(),
955 FetchResult::Buffered { meta, body } => f
956 .debug_struct("FetchResult::Buffered")
957 .field("meta", meta)
958 .field("body_len", &body.len())
959 .finish(),
960 FetchResult::Error(e) => f.debug_tuple("FetchResult::Error").field(e).finish(),
961 }
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968 use cow_utils::CowUtils;
969 use tokio::io::AsyncReadExt;
970
971 #[tokio::test(flavor = "current_thread")]
972 async fn bodystream_from_bytes_reads_all() {
973 let data = Bytes::from_static(b"hello world");
974 let mut s = BodyStream::from_bytes(data.clone());
975 assert_eq!(s.len, Some(11));
976 assert!(s.is_seekable);
977 assert!(s.clonable);
978
979 let mut out = Vec::new();
980 s.read_to_end(&mut out).await.unwrap();
981 assert_eq!(&out[..], &data[..]);
982
983 let n = s.read(&mut [0u8; 8]).await.unwrap();
984 assert_eq!(n, 0);
985 }
986
987 #[test]
988 fn stream_body_reports_len_and_no_bytes() {
989 let sized = RequestBody::stream(|| Ok(Box::pin(&b""[..]) as BoxedAsyncRead), Some(3));
990 assert_eq!(sized.len(), Some(3));
991 assert!(sized.as_bytes().is_none());
992 assert!(!sized.is_empty());
993
994 let unsized_body = RequestBody::stream(|| Ok(Box::pin(&b""[..]) as BoxedAsyncRead), None);
995 assert_eq!(unsized_body.len(), None);
996 assert!(!unsized_body.is_empty());
997
998 let buffered = RequestBody::bytes(&b"abc"[..]);
999 assert_eq!(buffered.len(), Some(3));
1000 assert_eq!(buffered.as_bytes().map(|b| b.len()), Some(3));
1001 }
1002
1003 #[test]
1004 fn builder_decodes_by_default() {
1005 let fr =
1006 FetchRequest::builder(Method::GET, Url::parse("https://example.org").unwrap()).build();
1007 assert!(fr.auto_decode);
1008 }
1009
1010 #[test]
1011 fn fetch_request_generate_get_and_headers() {
1012 let mut fr = FetchRequest::builder(
1013 Method::default(),
1014 Url::parse("https://example.org/a/b#frag").unwrap(),
1015 )
1016 .build();
1017 fr.headers
1018 .insert(header::RANGE, "bytes=0-99".parse().unwrap());
1019 fr.headers
1020 .insert(header::ACCEPT, "text/html".parse().unwrap());
1021 fr.headers
1022 .insert(header::ACCEPT_LANGUAGE, "en-US".parse().unwrap());
1023 fr.headers
1024 .insert(header::ACCEPT_ENCODING, "gzip".parse().unwrap());
1025 fr.headers
1026 .insert(header::AUTHORIZATION, "Bearer abc".parse().unwrap());
1027 fr.headers
1028 .insert(header::COOKIE, "a=1; b=2".parse().unwrap());
1029
1030 let key = fr.generate_request_key().expect("GET should produce a key");
1031
1032 let url_norm = normalize_url(&fr.url);
1033 let auth_hash = format!("{:x}", short_hash(b"Bearer abc"));
1034 let cookie_hash = format!("{:x}", short_hash(b"a=1; b=2"));
1035 let expected = format!(
1036 "M={};U={};R={};A={};AL={};AE={};Auth={};C={};MC=n;Ref=n;FM=empty:no-cors:n:-;Cred=include",
1040 fr.method, url_norm, "bytes=0-99", "text/html", "en-US", "gzip", auth_hash, cookie_hash
1041 );
1042
1043 assert_eq!(key, expected);
1044 assert!(key.starts_with("M=GET;U=https://example.org/a/b"));
1045 assert!(!key.contains("#frag"));
1046 }
1047
1048 #[test]
1052 fn coalescing_key_separates_secure_from_insecure_initiators() {
1053 let target = Url::parse("http://cdn.example.org/a.js").unwrap();
1054 let key_for = |origin: Option<&str>| {
1055 let mut b = FetchRequest::builder(Method::GET, target.clone());
1056 if let Some(o) = origin {
1057 b = b.with_origin(Url::parse(o).unwrap().origin());
1058 }
1059 b.build().generate_request_key().unwrap()
1060 };
1061
1062 let secure = key_for(Some("https://a.example.com"));
1063 let insecure = key_for(Some("http://b.example.com"));
1064 let none = key_for(None);
1065
1066 assert_ne!(secure, insecure);
1067 assert_ne!(insecure, none);
1070 assert_ne!(secure, key_for(Some("https://c.example.com")));
1071 }
1072
1073 #[test]
1076 fn coalescing_key_separates_per_request_policy_overrides() {
1077 let target = Url::parse("http://cdn.example.org/a.js").unwrap();
1078 let origin = Url::parse("https://example.com").unwrap().origin();
1079 let key_for = |policy: Option<MixedContentPolicy>| {
1080 let mut b =
1081 FetchRequest::builder(Method::GET, target.clone()).with_origin(origin.clone());
1082 if let Some(p) = policy {
1083 b = b.with_mixed_content(p);
1084 }
1085 b.build().generate_request_key().unwrap()
1086 };
1087
1088 let keys = [
1089 key_for(None),
1090 key_for(Some(MixedContentPolicy::Allow)),
1091 key_for(Some(MixedContentPolicy::Upgrade)),
1092 key_for(Some(MixedContentPolicy::Block)),
1093 ];
1094 for (i, a) in keys.iter().enumerate() {
1095 for b in &keys[i + 1..] {
1096 assert_ne!(a, b, "each policy reaches a different verdict");
1097 }
1098 }
1099 }
1100
1101 #[test]
1108 fn coalescing_key_does_not_trust_an_https_initial_url() {
1109 let target = Url::parse("https://redirector.example.org/r").unwrap();
1110 let key = |origin: Option<&str>| {
1111 let mut b = FetchRequest::builder(Method::GET, target.clone());
1112 if let Some(o) = origin {
1113 b = b.with_origin(Url::parse(o).unwrap().origin());
1114 }
1115 b.build().generate_request_key().unwrap()
1116 };
1117
1118 assert_ne!(
1119 key(Some("https://example.com")),
1120 key(None),
1121 "a secure-origin request must not share a bucket with an unprotected one, \
1122 however trustworthy the initial URL looks"
1123 );
1124 }
1125
1126 #[test]
1129 fn coalescing_key_separates_different_referrers() {
1130 let target = Url::parse("https://cdn.example.org/a.js").unwrap();
1131 let key = |referrer: Option<&str>, policy: ReferrerPolicy| {
1132 let mut b =
1133 FetchRequest::builder(Method::GET, target.clone()).with_referrer_policy(policy);
1134 if let Some(r) = referrer {
1135 b = b.with_referrer(Url::parse(r).unwrap());
1136 }
1137 b.build().generate_request_key().unwrap()
1138 };
1139 let default = ReferrerPolicy::default();
1140
1141 assert_ne!(
1142 key(Some("https://a.example.com/x"), default),
1143 key(Some("https://b.example.com/y"), default)
1144 );
1145 assert_ne!(
1147 key(Some("https://a.example.com/x"), default),
1148 key(Some("https://a.example.com/x"), ReferrerPolicy::UnsafeUrl)
1149 );
1150 assert_eq!(
1152 key(Some("https://a.example.com/x"), default),
1153 key(Some("https://a.example.com/x"), default)
1154 );
1155 assert_eq!(key(None, default), key(None, ReferrerPolicy::UnsafeUrl));
1157 assert_eq!(
1158 key(None, default),
1159 key(Some("https://a.example.com/x"), ReferrerPolicy::NoReferrer)
1160 );
1161 }
1162
1163 #[test]
1169 fn coalescing_key_is_not_derived_from_the_first_hop_value() {
1170 let target = Url::parse("https://other.example.org/r").unwrap();
1171 let key = |referrer: &str| {
1172 FetchRequest::builder(Method::GET, target.clone())
1173 .with_referrer(Url::parse(referrer).unwrap())
1174 .build()
1175 .generate_request_key()
1176 .unwrap()
1177 };
1178
1179 let (a, b) = ("https://example.com/page-a", "https://example.com/page-b");
1180 let policy = ReferrerPolicy::default();
1182 let hop0 = |r: &str| {
1183 referrer::determine(&Url::parse(r).unwrap(), policy, &target).map(|u| u.to_string())
1184 };
1185 assert_eq!(hop0(a), hop0(b));
1186 assert_ne!(key(a), key(b));
1188 }
1189
1190 #[test]
1193 fn coalescing_key_accounts_for_a_hand_set_referer_header() {
1194 let target = Url::parse("https://cdn.example.org/a.js").unwrap();
1195 let key = |manual: Option<&str>| {
1196 let mut req = FetchRequest::builder(Method::GET, target.clone()).build();
1197 if let Some(value) = manual {
1198 req.headers.insert(header::REFERER, value.parse().unwrap());
1199 }
1200 req.generate_request_key().unwrap()
1201 };
1202
1203 assert_ne!(key(Some("https://a.example.com/x")), key(None));
1204 assert_ne!(
1205 key(Some("https://a.example.com/x")),
1206 key(Some("https://b.example.com/y"))
1207 );
1208 assert_eq!(
1209 key(Some("https://a.example.com/x")),
1210 key(Some("https://a.example.com/x"))
1211 );
1212 }
1213
1214 #[test]
1217 fn coalescing_key_accounts_for_fetch_metadata() {
1218 let target = Url::parse("https://cdn.example.org/a.js").unwrap();
1219 let key = |dest: RequestDestination, mode: RequestMode, origin: Option<&str>| {
1220 let mut b = FetchRequest::builder(Method::GET, target.clone())
1221 .with_destination(dest)
1222 .with_mode(mode);
1223 if let Some(o) = origin {
1224 b = b.with_origin(Url::parse(o).unwrap().origin());
1225 }
1226 b.build().generate_request_key().unwrap()
1227 };
1228
1229 let (dest, mode) = (RequestDestination::default(), RequestMode::default());
1230 assert_ne!(
1232 key(RequestDestination::Script, mode, None),
1233 key(RequestDestination::Image, mode, None)
1234 );
1235 assert_ne!(
1236 key(dest, RequestMode::NoCors, None),
1237 key(dest, RequestMode::Cors, None)
1238 );
1239 assert_ne!(
1242 key(dest, mode, Some("https://a.example.com")),
1243 key(dest, mode, Some("https://b.example.com"))
1244 );
1245 assert_ne!(
1246 key(dest, mode, Some("https://a.example.com")),
1247 key(dest, mode, None)
1248 );
1249 assert_eq!(
1251 key(
1252 RequestDestination::Script,
1253 mode,
1254 Some("https://a.example.com")
1255 ),
1256 key(
1257 RequestDestination::Script,
1258 mode,
1259 Some("https://a.example.com")
1260 )
1261 );
1262 }
1263
1264 #[test]
1267 fn coalescing_key_accounts_for_a_hand_set_origin_header() {
1268 let target = Url::parse("https://cdn.example.org/a.js").unwrap();
1269 let key = |manual: Option<&str>| {
1270 let mut req = FetchRequest::builder(Method::GET, target.clone()).build();
1271 if let Some(value) = manual {
1272 req.headers.insert(header::ORIGIN, value.parse().unwrap());
1273 }
1274 req.generate_request_key().unwrap()
1275 };
1276
1277 assert_ne!(key(Some("https://a.example.com")), key(None));
1278 assert_ne!(
1279 key(Some("https://a.example.com")),
1280 key(Some("https://b.example.com"))
1281 );
1282 assert_eq!(
1283 key(Some("https://a.example.com")),
1284 key(Some("https://a.example.com"))
1285 );
1286 }
1287
1288 #[test]
1289 fn fetch_request_generate_post_is_none() {
1290 let mut fr = FetchRequest::builder(
1291 Method::default(),
1292 Url::parse("https://example.org/").unwrap(),
1293 )
1294 .build();
1295 fr.method = Method::POST;
1296 assert!(fr.generate_request_key().is_none());
1297 }
1298
1299 #[test]
1300 fn priority_display_is_stable() {
1301 assert_eq!(format!("{}", Priority::High), "High");
1302 assert_eq!(format!("{}", Priority::Normal), "Normal");
1303 assert_eq!(format!("{}", Priority::Low), "Low");
1304 assert_eq!(format!("{}", Priority::Idle), "Idle");
1305 }
1306
1307 #[test]
1308 fn neterror_helpers_work() {
1309 let io = NetError::Timeout("oops".into()).to_io();
1310 assert_eq!(io.kind(), std::io::ErrorKind::Other);
1311 assert!(io.to_string().cow_to_ascii_lowercase().contains("timeout"));
1312
1313 let ne = NetError::from_anyhow(anyhow::anyhow!("boom"));
1314 assert!(matches!(ne, NetError::Read(_)));
1315 }
1316
1317 #[test]
1318 fn net_error_redirect_formats_with_redirect_prefix() {
1319 let e = NetError::Redirect(Arc::new(anyhow::anyhow!("too many redirects")));
1320 assert!(e.to_string().contains("redirect"));
1321 }
1322
1323 #[tokio::test(flavor = "current_thread")]
1324 async fn body_stream_new_creates_non_seekable_stream() {
1325 use tokio::io::AsyncReadExt;
1326 let mut s = BodyStream::new(Box::pin(tokio::io::empty()), Some(0));
1327 assert_eq!(s.len, Some(0));
1328 assert!(!s.is_seekable);
1329 assert!(!s.clonable);
1330 let n = s.read(&mut [0u8; 4]).await.unwrap();
1331 assert_eq!(n, 0);
1332 }
1333
1334 #[test]
1335 fn fetch_result_meta_returns_none_for_error() {
1336 let e = FetchResult::Error(NetError::Cancelled("x".into()));
1337 assert!(e.meta().is_none());
1338 assert!(e.is_error());
1339 }
1340
1341 #[tokio::test(flavor = "current_thread")]
1342 async fn fetch_result_meta_returns_some_for_stream_and_buffered() {
1343 use crate::net::shared_body::SharedBody;
1344 use crate::types::PeekBuf;
1345 use http::HeaderMap;
1346
1347 let meta = FetchResultMeta {
1348 final_url: Url::parse("http://example.com/").unwrap(),
1349 status: 200,
1350 status_text: "OK".into(),
1351 headers: HeaderMap::new(),
1352 content_length: None,
1353 content_type: None,
1354 has_body: false,
1355 tainting: ResponseTainting::Basic,
1356 };
1357
1358 let buffered = FetchResult::Buffered {
1359 meta: meta.clone(),
1360 body: bytes::Bytes::new(),
1361 };
1362 assert_eq!(buffered.meta().unwrap().status, 200);
1363 assert!(!buffered.is_error());
1364 assert!(format!("{:?}", buffered).contains("Buffered"));
1365
1366 let stream = FetchResult::Stream {
1367 meta: meta.clone(),
1368 peek_buf: PeekBuf::empty(),
1369 shared: Arc::new(SharedBody::new(1)),
1370 };
1371 assert_eq!(stream.meta().unwrap().status, 200);
1372 assert!(format!("{:?}", stream).contains("Stream"));
1373 }
1374
1375 #[test]
1376 fn fetch_request_builder_builds_correctly() {
1377 let mut headers = HeaderMap::new();
1378 headers.insert("ACCEPT", "text/html".parse().unwrap());
1379 headers.insert("CONTENT_TYPE", "application/json".parse().unwrap());
1380
1381 let reference = RequestReference::default();
1382 let req_id = RequestId::new();
1383 let priority = Priority::High;
1384 let initiator = Initiator::Application;
1385 let kind = ResourceKind::Asset;
1386 let body = RequestBody::json(r#"{"key": "value"}"#);
1387
1388 let request =
1389 FetchRequest::builder(Method::POST, Url::parse("https://example.com/api").unwrap())
1390 .with_reference(reference)
1391 .with_req_id(req_id)
1392 .with_priority(priority)
1393 .with_initiator(initiator)
1394 .with_kind(kind)
1395 .with_headers(headers)
1396 .with_streaming(true)
1397 .with_auto_decode(true)
1398 .with_max_bytes(1024)
1399 .with_body(body)
1400 .build();
1401
1402 assert_eq!(request.reference, reference);
1403 assert_eq!(request.req_id, req_id);
1404 assert_eq!(request.priority, priority);
1405 assert_eq!(request.initiator, initiator);
1406 assert_eq!(request.kind, kind);
1407 assert!(request.streaming);
1408 assert!(request.auto_decode);
1409 assert_eq!(request.max_bytes, Some(1024));
1410 assert_eq!(
1411 request.body.as_ref().unwrap().content_type,
1412 Some("application/json".into())
1413 );
1414
1415 assert_eq!(request.url.as_str(), "https://example.com/api");
1416 assert_eq!(request.method, Method::POST);
1417 assert!(request.headers.contains_key("ACCEPT"));
1418 assert!(request.headers.contains_key("CONTENT_TYPE"));
1419 }
1420}