1use crate::net::request_ref::RequestReference;
4use crate::net::shared_body::SharedBody;
5use crate::net::utils::{normalize_url, short_hash, BytesAsyncReader};
6use crate::types::{PeekBuf, RequestId};
7use bytes::Bytes;
8use http::{header, HeaderMap, Method};
9use std::fmt::{Debug, Display};
10use std::hash::Hash;
11use std::pin::Pin;
12use std::sync::Arc;
13use tokio::io::{AsyncRead, ReadBuf};
14use tokio_util::sync::CancellationToken;
15use url::Url;
16
17#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
20pub enum Priority {
21 High,
23 #[default]
25 Normal,
26 Low,
28 Idle,
30}
31
32impl Display for Priority {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 let s = match self {
35 Priority::High => "High",
36 Priority::Normal => "Normal",
37 Priority::Low => "Low",
38 Priority::Idle => "Idle",
39 };
40 f.write_str(s)
41 }
42}
43
44#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
50pub enum ResourceKind {
51 #[default]
53 Primary,
54 Asset,
56 Other,
58}
59
60#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
65pub enum Initiator {
66 #[default]
68 User,
69 Application,
71 Other,
73}
74
75#[derive(Clone, Debug)]
77pub struct FetchResultMeta {
78 pub final_url: Url,
80 pub status: u16,
82 pub status_text: String,
84 pub headers: HeaderMap,
86 pub content_length: Option<u64>,
88 pub content_type: Option<String>,
90 pub has_body: bool,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct FetchKeyData {
98 pub url: Url,
100 pub method: Method,
102 pub headers: HeaderMap,
104}
105
106impl Hash for FetchKeyData {
107 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
108 if let Some(key) = self.generate() {
109 key.hash(state);
110 }
111 }
112}
113
114impl Display for FetchKeyData {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}", self.url)
117 }
118}
119
120impl FetchKeyData {
121 pub fn new(url: Url) -> Self {
123 Self {
124 url,
125 method: Method::GET,
126 headers: HeaderMap::new(),
127 }
128 }
129
130 pub fn generate(&self) -> Option<String> {
132 match self.method {
133 Method::GET | Method::HEAD => {}
134 _ => return None,
135 }
136
137 let url = normalize_url(&self.url);
138 let h = &self.headers;
139
140 let range = h
141 .get(header::RANGE)
142 .and_then(|v| v.to_str().ok())
143 .unwrap_or("");
144 let accept = h
145 .get(header::ACCEPT)
146 .and_then(|v| v.to_str().ok())
147 .unwrap_or("");
148 let accept_enc = h
149 .get(header::ACCEPT_ENCODING)
150 .and_then(|v| v.to_str().ok())
151 .unwrap_or("");
152 let accept_lang = h
153 .get(header::ACCEPT_LANGUAGE)
154 .and_then(|v| v.to_str().ok())
155 .unwrap_or("");
156
157 let auth_hash = h
158 .get(header::AUTHORIZATION)
159 .map(|v| format!("{:x}", short_hash(v.as_bytes())))
160 .unwrap_or_default();
161 let cookie_hash = h
162 .get(header::COOKIE)
163 .map(|v| format!("{:x}", short_hash(v.as_bytes())))
164 .unwrap_or_default();
165
166 Some(format!(
167 "M={};U={};R={};A={};AL={};AE={};Auth={};C={}",
168 self.method, url, range, accept, accept_lang, accept_enc, auth_hash, cookie_hash
169 ))
170 }
171}
172
173#[derive(Debug, thiserror::Error, Clone)]
175pub enum NetError {
176 #[error("net error: reqwest: {0}")]
178 Reqwest(#[from] Arc<reqwest::Error>),
179
180 #[error("net error: redirect: {0}")]
182 Redirect(Arc<anyhow::Error>),
183
184 #[error("net error: I/O: {0}")]
186 Io(#[from] Arc<std::io::Error>),
187
188 #[error("net error: cancelled: {0}")]
190 Cancelled(String),
191
192 #[error(transparent)]
194 Read(Arc<anyhow::Error>),
195
196 #[error(transparent)]
198 Other(Arc<anyhow::Error>),
199
200 #[error("net error: timeout: {0}")]
202 Timeout(String),
203}
204
205impl From<std::io::Error> for NetError {
206 fn from(e: std::io::Error) -> Self {
207 NetError::Io(Arc::new(e))
208 }
209}
210
211impl NetError {
212 pub fn to_io(&self) -> std::io::Error {
216 std::io::Error::other(self.clone())
217 }
218
219 pub fn from_anyhow(e: anyhow::Error) -> Self {
221 Self::Read(Arc::new(e))
222 }
223}
224
225pub struct BodyStream {
227 inner: Pin<Box<dyn AsyncRead + Send + 'static>>,
229 pub len: Option<u64>,
231 pub is_seekable: bool,
233 pub clonable: bool,
235}
236
237impl Debug for BodyStream {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 f.debug_struct("BodyStream")
240 .field("len", &self.len)
241 .field("is_seekable", &self.is_seekable)
242 .field("clonable", &self.clonable)
243 .finish()
244 }
245}
246
247impl BodyStream {
248 pub fn new(inner: Pin<Box<dyn AsyncRead + Send + 'static>>, len: Option<u64>) -> Self {
250 Self {
251 inner,
252 len,
253 is_seekable: false,
254 clonable: false,
255 }
256 }
257
258 pub fn from_bytes(bytes: Bytes) -> Self {
260 let len = bytes.len() as u64;
261 let reader = Box::pin(BytesAsyncReader {
262 data: bytes,
263 pos: 0,
264 });
265 Self {
266 inner: reader,
267 len: Some(len),
268 is_seekable: true, clonable: true, }
271 }
272}
273
274impl AsyncRead for BodyStream {
275 fn poll_read(
276 mut self: Pin<&mut Self>,
277 cx: &mut std::task::Context<'_>,
278 buf: &mut ReadBuf<'_>,
279 ) -> std::task::Poll<std::io::Result<()>> {
280 self.inner.as_mut().poll_read(cx, buf)
281 }
282}
283
284#[derive(Clone)]
289pub struct FetchHandle {
290 pub req_id: RequestId,
292 pub key: FetchKeyData,
294 pub cancel: CancellationToken,
296}
297
298impl Debug for FetchHandle {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 f.debug_struct("FetchHandle")
301 .field("req_id", &self.req_id)
302 .field("key", &self.key)
303 .field("cancel", &self.cancel)
304 .finish()
305 }
306}
307
308#[derive(Debug, Clone, Default)]
314pub struct RequestBody {
315 pub bytes: Bytes,
317 pub content_type: Option<String>,
320}
321
322impl RequestBody {
323 pub fn bytes(b: impl Into<Bytes>) -> Self {
325 Self {
326 bytes: b.into(),
327 content_type: None,
328 }
329 }
330
331 pub fn json(b: impl Into<Bytes>) -> Self {
333 Self {
334 bytes: b.into(),
335 content_type: Some("application/json".into()),
336 }
337 }
338
339 pub fn form(b: impl Into<Bytes>) -> Self {
341 Self {
342 bytes: b.into(),
343 content_type: Some("application/x-www-form-urlencoded".into()),
344 }
345 }
346
347 pub fn text(s: impl Into<String>) -> Self {
349 Self {
350 bytes: Bytes::from(s.into().into_bytes()),
351 content_type: Some("text/plain; charset=utf-8".into()),
352 }
353 }
354
355 pub fn is_empty(&self) -> bool {
357 self.bytes.is_empty()
358 }
359
360 pub fn len(&self) -> usize {
362 self.bytes.len()
363 }
364}
365
366#[derive(Debug, Clone)]
368pub struct FetchRequest {
369 pub reference: RequestReference,
371 pub req_id: RequestId,
373 pub key_data: FetchKeyData,
375 pub priority: Priority,
377 pub initiator: Initiator,
379 pub kind: ResourceKind,
381 pub streaming: bool,
383 pub auto_decode: bool,
385 pub max_bytes: Option<usize>,
387 pub body: Option<RequestBody>,
390}
391
392impl FetchRequest {
393 pub fn builder(method: Method, url: impl Into<Url>) -> FetchRequestBuilder {
395 FetchRequestBuilder::new(method, url)
396 }
397}
398
399pub struct FetchRequestBuilder {
404 reference: RequestReference,
405 req_id: RequestId,
406 key_data: FetchKeyData,
407 priority: Priority,
408 initiator: Initiator,
409 kind: ResourceKind,
410 streaming: bool,
411 auto_decode: bool,
412 max_bytes: Option<usize>,
413 body: Option<RequestBody>,
414}
415
416impl FetchRequestBuilder {
417 pub fn new(method: Method, url: impl Into<Url>) -> Self {
419 Self {
420 key_data: FetchKeyData {
421 url: url.into(),
422 method,
423 headers: HeaderMap::default(),
424 },
425 reference: RequestReference::default(),
426 req_id: RequestId {
427 ..Default::default()
428 },
429 priority: Priority::default(),
430 initiator: Initiator::default(),
431 kind: ResourceKind::default(),
432 streaming: false,
433 auto_decode: false,
434 max_bytes: None,
435 body: None,
436 }
437 }
438
439 pub fn with_reference(mut self, reference: RequestReference) -> Self {
441 self.reference = reference;
442 self
443 }
444
445 pub fn with_req_id(mut self, req_id: RequestId) -> Self {
447 self.req_id = req_id;
448 self
449 }
450
451 pub fn with_priority(mut self, priority: Priority) -> Self {
453 self.priority = priority;
454 self
455 }
456
457 pub fn with_initiator(mut self, initiator: Initiator) -> Self {
459 self.initiator = initiator;
460 self
461 }
462
463 pub fn with_kind(mut self, kind: ResourceKind) -> Self {
465 self.kind = kind;
466 self
467 }
468
469 pub fn with_streaming(mut self, streaming: bool) -> Self {
471 self.streaming = streaming;
472 self
473 }
474
475 pub fn with_auto_decode(mut self, auto_decode: bool) -> Self {
477 self.auto_decode = auto_decode;
478 self
479 }
480
481 pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
483 self.max_bytes = Some(max_bytes);
484 self
485 }
486
487 pub fn with_body(mut self, body: RequestBody) -> Self {
489 self.body = Some(body);
490 self
491 }
492
493 pub fn with_url(mut self, url: impl Into<Url>) -> Self {
495 self.key_data.url = url.into();
496 self
497 }
498
499 pub fn with_method(mut self, method: Method) -> Self {
501 self.key_data.method = method;
502 self
503 }
504
505 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
507 self.key_data.headers = headers;
508 self
509 }
510
511 pub fn build(self) -> FetchRequest {
513 FetchRequest {
514 reference: self.reference,
515 req_id: self.req_id,
516 key_data: self.key_data,
517 priority: self.priority,
518 initiator: self.initiator,
519 kind: self.kind,
520 streaming: self.streaming,
521 auto_decode: self.auto_decode,
522 max_bytes: self.max_bytes,
523 body: self.body,
524 }
525 }
526}
527
528#[derive(Clone)]
530pub enum FetchResult {
531 Stream {
533 meta: FetchResultMeta,
535 peek_buf: PeekBuf,
537 shared: Arc<SharedBody>,
539 },
540 Buffered {
542 meta: FetchResultMeta,
544 body: Bytes,
546 },
547 Error(NetError),
549}
550
551impl FetchResult {
552 pub fn is_error(&self) -> bool {
554 matches!(self, FetchResult::Error(_))
555 }
556
557 pub fn meta(&self) -> Option<&FetchResultMeta> {
559 match self {
560 FetchResult::Stream { meta, .. } => Some(meta),
561 FetchResult::Buffered { meta, .. } => Some(meta),
562 FetchResult::Error(_) => None,
563 }
564 }
565}
566
567impl Debug for FetchResult {
568 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569 match self {
570 FetchResult::Stream { meta, .. } => f
571 .debug_struct("FetchResult::Stream")
572 .field("meta", meta)
573 .finish(),
574 FetchResult::Buffered { meta, body } => f
575 .debug_struct("FetchResult::Buffered")
576 .field("meta", meta)
577 .field("body_len", &body.len())
578 .finish(),
579 FetchResult::Error(e) => f.debug_tuple("FetchResult::Error").field(e).finish(),
580 }
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587 use cow_utils::CowUtils;
588 use tokio::io::AsyncReadExt;
589
590 #[tokio::test(flavor = "current_thread")]
591 async fn bodystream_from_bytes_reads_all() {
592 let data = Bytes::from_static(b"hello world");
593 let mut s = BodyStream::from_bytes(data.clone());
594 assert_eq!(s.len, Some(11));
595 assert!(s.is_seekable);
596 assert!(s.clonable);
597
598 let mut out = Vec::new();
599 s.read_to_end(&mut out).await.unwrap();
600 assert_eq!(&out[..], &data[..]);
601
602 let n = s.read(&mut [0u8; 8]).await.unwrap();
603 assert_eq!(n, 0);
604 }
605
606 #[test]
607 fn fetch_key_generate_get_and_headers() {
608 let mut fk = FetchKeyData::new(Url::parse("https://example.org/a/b#frag").unwrap());
609 fk.headers
610 .insert(header::RANGE, "bytes=0-99".parse().unwrap());
611 fk.headers
612 .insert(header::ACCEPT, "text/html".parse().unwrap());
613 fk.headers
614 .insert(header::ACCEPT_LANGUAGE, "en-US".parse().unwrap());
615 fk.headers
616 .insert(header::ACCEPT_ENCODING, "gzip".parse().unwrap());
617 fk.headers
618 .insert(header::AUTHORIZATION, "Bearer abc".parse().unwrap());
619 fk.headers
620 .insert(header::COOKIE, "a=1; b=2".parse().unwrap());
621
622 let key = fk.generate().expect("GET should produce a key");
623
624 let url_norm = normalize_url(&fk.url);
625 let auth_hash = format!("{:x}", short_hash(b"Bearer abc"));
626 let cookie_hash = format!("{:x}", short_hash(b"a=1; b=2"));
627 let expected = format!(
628 "M={};U={};R={};A={};AL={};AE={};Auth={};C={}",
629 fk.method, url_norm, "bytes=0-99", "text/html", "en-US", "gzip", auth_hash, cookie_hash
630 );
631
632 assert_eq!(key, expected);
633 assert!(key.starts_with("M=GET;U=https://example.org/a/b"));
634 assert!(!key.contains("#frag"));
635 }
636
637 #[test]
638 fn fetch_key_generate_post_is_none() {
639 let mut fk = FetchKeyData::new(Url::parse("https://example.org/").unwrap());
640 fk.method = Method::POST;
641 assert!(fk.generate().is_none());
642 }
643
644 #[test]
645 fn priority_display_is_stable() {
646 assert_eq!(format!("{}", Priority::High), "High");
647 assert_eq!(format!("{}", Priority::Normal), "Normal");
648 assert_eq!(format!("{}", Priority::Low), "Low");
649 assert_eq!(format!("{}", Priority::Idle), "Idle");
650 }
651
652 #[test]
653 fn neterror_helpers_work() {
654 let io = NetError::Timeout("oops".into()).to_io();
655 assert_eq!(io.kind(), std::io::ErrorKind::Other);
656 assert!(io.to_string().cow_to_ascii_lowercase().contains("timeout"));
657
658 let ne = NetError::from_anyhow(anyhow::anyhow!("boom"));
659 assert!(matches!(ne, NetError::Read(_)));
660 }
661
662 #[test]
663 fn net_error_redirect_formats_with_redirect_prefix() {
664 let e = NetError::Redirect(Arc::new(anyhow::anyhow!("too many redirects")));
665 assert!(e.to_string().contains("redirect"));
666 }
667
668 #[test]
669 fn fetch_key_data_display_shows_url() {
670 let key = FetchKeyData::new(Url::parse("http://example.com/path").unwrap());
671 assert_eq!(format!("{}", key), "http://example.com/path");
672 }
673
674 #[test]
675 fn fetch_key_data_is_usable_as_hash_map_key() {
676 use std::collections::HashMap;
677 let key = FetchKeyData::new(Url::parse("http://example.com/").unwrap());
678 let mut map = HashMap::new();
679 map.insert(key.clone(), 42u32);
680 assert_eq!(map.get(&key), Some(&42));
681 }
682
683 #[tokio::test(flavor = "current_thread")]
684 async fn body_stream_new_creates_non_seekable_stream() {
685 use tokio::io::AsyncReadExt;
686 let mut s = BodyStream::new(Box::pin(tokio::io::empty()), Some(0));
687 assert_eq!(s.len, Some(0));
688 assert!(!s.is_seekable);
689 assert!(!s.clonable);
690 let n = s.read(&mut [0u8; 4]).await.unwrap();
691 assert_eq!(n, 0);
692 }
693
694 #[test]
695 fn fetch_handle_implements_debug() {
696 let key = FetchKeyData::new(Url::parse("http://example.com/").unwrap());
697 let req_id = crate::types::RequestId::new();
698 let handle = FetchHandle {
699 req_id,
700 key,
701 cancel: tokio_util::sync::CancellationToken::new(),
702 };
703 assert!(format!("{:?}", handle).contains("FetchHandle"));
704 }
705
706 #[test]
707 fn fetch_result_meta_returns_none_for_error() {
708 let e = FetchResult::Error(NetError::Cancelled("x".into()));
709 assert!(e.meta().is_none());
710 assert!(e.is_error());
711 }
712
713 #[tokio::test(flavor = "current_thread")]
714 async fn fetch_result_meta_returns_some_for_stream_and_buffered() {
715 use crate::net::shared_body::SharedBody;
716 use crate::types::PeekBuf;
717 use http::HeaderMap;
718
719 let meta = FetchResultMeta {
720 final_url: Url::parse("http://example.com/").unwrap(),
721 status: 200,
722 status_text: "OK".into(),
723 headers: HeaderMap::new(),
724 content_length: None,
725 content_type: None,
726 has_body: false,
727 };
728
729 let buffered = FetchResult::Buffered {
730 meta: meta.clone(),
731 body: bytes::Bytes::new(),
732 };
733 assert_eq!(buffered.meta().unwrap().status, 200);
734 assert!(!buffered.is_error());
735 assert!(format!("{:?}", buffered).contains("Buffered"));
736
737 let stream = FetchResult::Stream {
738 meta: meta.clone(),
739 peek_buf: PeekBuf::empty(),
740 shared: Arc::new(SharedBody::new(1)),
741 };
742 assert_eq!(stream.meta().unwrap().status, 200);
743 assert!(format!("{:?}", stream).contains("Stream"));
744 }
745
746 #[test]
747 fn fetch_request_builder_builds_correctly() {
748 let mut headers = HeaderMap::new();
749 headers.insert("ACCEPT", "text/html".parse().unwrap());
750 headers.insert("CONTENT_TYPE", "application/json".parse().unwrap());
751
752 let reference = RequestReference::default();
753 let req_id = RequestId::new();
754 let priority = Priority::High;
755 let initiator = Initiator::Application;
756 let kind = ResourceKind::Asset;
757 let body = RequestBody::json(r#"{"key": "value"}"#);
758
759 let request =
760 FetchRequest::builder(Method::POST, Url::parse("https://example.com/api").unwrap())
761 .with_reference(reference)
762 .with_req_id(req_id)
763 .with_priority(priority)
764 .with_initiator(initiator)
765 .with_kind(kind)
766 .with_headers(headers)
767 .with_streaming(true)
768 .with_auto_decode(true)
769 .with_max_bytes(1024)
770 .with_body(body)
771 .build();
772
773 assert_eq!(request.reference, reference);
774 assert_eq!(request.req_id, req_id);
775 assert_eq!(request.priority, priority);
776 assert_eq!(request.initiator, initiator);
777 assert_eq!(request.kind, kind);
778 assert!(request.streaming);
779 assert!(request.auto_decode);
780 assert_eq!(request.max_bytes, Some(1024));
781 assert_eq!(
782 request.body.as_ref().unwrap().content_type,
783 Some("application/json".into())
784 );
785
786 let key_data = &request.key_data;
787 assert_eq!(key_data.url.as_str(), "https://example.com/api");
788 assert_eq!(key_data.method, Method::POST);
789 assert!(key_data.headers.contains_key("ACCEPT"));
790 assert!(key_data.headers.contains_key("CONTENT_TYPE"));
791 }
792}