Skip to main content

gosub_sonar/net/
types.rs

1//! Core types for fetch requests, responses, errors, and priorities.
2
3use 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/// Priority of the scheduled request. Documents usually have high priority, while images have low.
18/// Currently, the scheduler uses a round-robin system to load resources
19#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
20pub enum Priority {
21    /// Fetched before all lower priorities (e.g. primary documents)
22    High,
23    /// Default priority for most resources
24    #[default]
25    Normal,
26    /// Fetched after normal-priority resources (e.g. images)
27    Low,
28    /// Only fetched when nothing else is pending (e.g. prefetches)
29    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/// Broad category of the resource being fetched.
45///
46/// Callers that need finer-grained classification can extend this at the
47/// application layer; the net crate only uses these values for logging and
48/// to pass them back through [`crate::net::fetcher_context::FetcherContext::observer_for`].
49#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
50pub enum ResourceKind {
51    /// Top-level or primary resource (e.g. a document, feed, or binary download)
52    #[default]
53    Primary,
54    /// Secondary asset loaded on behalf of a primary resource (e.g. image, font, script)
55    Asset,
56    /// Other or unspecified resource kind
57    Other,
58}
59
60/// Who or what triggered the fetch.
61///
62/// Used for logging and passed back through [`crate::net::fetcher_context::FetcherContext::observer_for`];
63/// the net crate does not alter scheduling based on this value.
64#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
65pub enum Initiator {
66    /// Triggered by a user action (e.g. address bar, link click, button)
67    #[default]
68    User,
69    /// Triggered programmatically by the application
70    Application,
71    /// Other or unspecified initiator
72    Other,
73}
74
75/// Metadata returned by the FetchResult
76#[derive(Clone, Debug)]
77pub struct FetchResultMeta {
78    /// Final URL after redirects
79    pub final_url: Url,
80    /// HTTP status code
81    pub status: u16,
82    /// HTTP status reason phrase
83    pub status_text: String,
84    /// Response headers
85    pub headers: HeaderMap,
86    /// Length of the content (if known from headers)
87    pub content_length: Option<u64>,
88    /// Content-Type header (if any)
89    pub content_type: Option<String>,
90    /// True if the response has a body (e.g. HEAD requests do not)
91    pub has_body: bool,
92}
93
94/// A fetch key data is a key that is used to find out if two requests want to fetch the same resource.
95/// If this is true, the requests are bundled so only once the resource will be fetched.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct FetchKeyData {
98    /// URL fetched
99    pub url: Url,
100    /// HTTP method used (GET, POST etc.)
101    pub method: Method,
102    /// HTTP headers
103    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    /// Creates a new fetch key data with the given URL, method GET and no headers
122    pub fn new(url: Url) -> Self {
123        Self {
124            url,
125            method: Method::GET,
126            headers: HeaderMap::new(),
127        }
128    }
129
130    /// Generates a key for coalescing in-flight requests based on the request's method, URL, and headers.
131    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/// Network-level errors.
174#[derive(Debug, thiserror::Error, Clone)]
175pub enum NetError {
176    /// Error reported by the underlying HTTP client
177    #[error("net error: reqwest: {0}")]
178    Reqwest(#[from] Arc<reqwest::Error>),
179
180    /// Redirect could not be followed (e.g. too many redirects, invalid target)
181    #[error("net error: redirect: {0}")]
182    Redirect(Arc<anyhow::Error>),
183
184    /// I/O error while transferring data
185    #[error("net error: I/O: {0}")]
186    Io(#[from] Arc<std::io::Error>),
187
188    /// Request was cancelled before it completed; the string describes why
189    #[error("net error: cancelled: {0}")]
190    Cancelled(String),
191
192    /// Error while reading the response body
193    #[error(transparent)]
194    Read(Arc<anyhow::Error>),
195
196    /// Any other error not covered by the variants above
197    #[error(transparent)]
198    Other(Arc<anyhow::Error>),
199
200    /// Request did not complete within the configured time limit
201    #[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    /// Wrap this error in an `io::Error`, carrying the typed error as the source so the other
213    /// side of an `AsyncRead` boundary can recover the original `NetError` (see
214    /// `stream_to_bytes`) instead of a stringified copy.
215    pub fn to_io(&self) -> std::io::Error {
216        std::io::Error::other(self.clone())
217    }
218
219    /// Wraps an [`anyhow::Error`] as a [`NetError::Read`]
220    pub fn from_anyhow(e: anyhow::Error) -> Self {
221        Self::Read(Arc::new(e))
222    }
223}
224
225/// A BodyStream is an async reader that can be used to read the body of a response.
226pub struct BodyStream {
227    /// Inner reader
228    inner: Pin<Box<dyn AsyncRead + Send + 'static>>,
229    /// Content length (if known)
230    pub len: Option<u64>,
231    /// True when the stream is seekable (most often not, unless it's backed by a memory buffer)
232    pub is_seekable: bool,
233    /// Can be cloned to create a new independent stream starting at the beginning
234    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    /// Creates a non-seekable, non-clonable stream from the given reader and optional length
249    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    /// Converts a series of bytes into a body stream
259    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, // It's a buffer so we can seek it
269            clonable: true,    // It's a buffer so we can clone it
270        }
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/// Handle identifying a submitted request, used to track and cancel it.
285///
286/// Created by the caller when using [`Fetcher::submit`](crate::Fetcher::submit); the
287/// higher-level `fetch` methods create one internally.
288#[derive(Clone)]
289pub struct FetchHandle {
290    /// Unique ID of this request (for logging and tracking)
291    pub req_id: RequestId,
292    /// Key data identifying the resource to fetch
293    pub key: FetchKeyData,
294    /// Cancellation token
295    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/// Body sent with a non-GET request (POST, PUT, PATCH, …).
309///
310/// The `content_type` field is automatically injected as a `Content-Type` header when the
311/// request headers do not already contain one. The caller is responsible for encoding the body
312/// correctly (JSON, form-encoding, multipart, etc.).
313#[derive(Debug, Clone, Default)]
314pub struct RequestBody {
315    /// Raw bytes to send.
316    pub bytes: Bytes,
317    /// Optional `Content-Type` value to inject (e.g. `"application/json"`).
318    /// Ignored if the request headers already set `Content-Type`.
319    pub content_type: Option<String>,
320}
321
322impl RequestBody {
323    /// Plain byte body with no automatic `Content-Type`.
324    pub fn bytes(b: impl Into<Bytes>) -> Self {
325        Self {
326            bytes: b.into(),
327            content_type: None,
328        }
329    }
330
331    /// `application/json` body.
332    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    /// `application/x-www-form-urlencoded` body.
340    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    /// `text/plain; charset=utf-8` body.
348    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    /// Returns true when the body contains no bytes
356    pub fn is_empty(&self) -> bool {
357        self.bytes.is_empty()
358    }
359
360    /// Returns the number of bytes in the body
361    pub fn len(&self) -> usize {
362        self.bytes.len()
363    }
364}
365
366/// A fetch request defines what needs to be fetched, how and where to send the result to
367#[derive(Debug, Clone)]
368pub struct FetchRequest {
369    /// Reference to what initiated this request (navigation, document, prefetch, background task)
370    pub reference: RequestReference,
371    /// Unique ID of this request (for logging and tracking)
372    pub req_id: RequestId,
373    /// Key data identifying the resource to fetch (URL, method, headers)
374    pub key_data: FetchKeyData,
375    /// Priority of this request
376    pub priority: Priority,
377    /// Who initiated this request
378    pub initiator: Initiator,
379    /// What kind of resource is being fetched
380    pub kind: ResourceKind,
381    /// Whether to stream the response body or buffer it fully before returning
382    pub streaming: bool,
383    /// Auto decode the request (if for instance, gzipped), or pass directly through to the caller
384    pub auto_decode: bool,
385    /// Maximum amount of (buffered) bytes we can fetch
386    pub max_bytes: Option<usize>,
387    /// Optional request body (for POST, PUT, PATCH, DELETE, etc.).
388    /// `None` for GET and HEAD requests.
389    pub body: Option<RequestBody>,
390}
391
392impl FetchRequest {
393    /// Starts building a request for the given method and URL
394    pub fn builder(method: Method, url: impl Into<Url>) -> FetchRequestBuilder {
395        FetchRequestBuilder::new(method, url)
396    }
397}
398
399/// Builder for [`FetchRequest`], created via [`FetchRequest::builder`].
400///
401/// All settings are optional; `build()` produces a buffered, non-decoding request
402/// with [`Priority::Normal`] unless configured otherwise.
403pub 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    /// Creates a builder for the given method and URL with default settings
418    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    /// Sets what initiated this request (navigation, document, prefetch, background task)
440    pub fn with_reference(mut self, reference: RequestReference) -> Self {
441        self.reference = reference;
442        self
443    }
444
445    /// Sets an explicit request ID instead of the generated one
446    pub fn with_req_id(mut self, req_id: RequestId) -> Self {
447        self.req_id = req_id;
448        self
449    }
450
451    /// Sets the scheduling priority (default: [`Priority::Normal`])
452    pub fn with_priority(mut self, priority: Priority) -> Self {
453        self.priority = priority;
454        self
455    }
456
457    /// Sets who initiated this request (default: [`Initiator::User`])
458    pub fn with_initiator(mut self, initiator: Initiator) -> Self {
459        self.initiator = initiator;
460        self
461    }
462
463    /// Sets the kind of resource being fetched (default: [`ResourceKind::Primary`])
464    pub fn with_kind(mut self, kind: ResourceKind) -> Self {
465        self.kind = kind;
466        self
467    }
468
469    /// Sets whether to stream the response body instead of buffering it (default: buffered)
470    pub fn with_streaming(mut self, streaming: bool) -> Self {
471        self.streaming = streaming;
472        self
473    }
474
475    /// Sets whether to transparently decode compressed responses (default: false)
476    pub fn with_auto_decode(mut self, auto_decode: bool) -> Self {
477        self.auto_decode = auto_decode;
478        self
479    }
480
481    /// Sets the maximum number of body bytes to buffer (default: unlimited)
482    pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
483        self.max_bytes = Some(max_bytes);
484        self
485    }
486
487    /// Sets the request body (for POST, PUT, PATCH, etc.)
488    pub fn with_body(mut self, body: RequestBody) -> Self {
489        self.body = Some(body);
490        self
491    }
492
493    /// Replaces the URL set by [`FetchRequestBuilder::new`]
494    pub fn with_url(mut self, url: impl Into<Url>) -> Self {
495        self.key_data.url = url.into();
496        self
497    }
498
499    /// Replaces the HTTP method set by [`FetchRequestBuilder::new`]
500    pub fn with_method(mut self, method: Method) -> Self {
501        self.key_data.method = method;
502        self
503    }
504
505    /// Sets the request headers
506    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
507        self.key_data.headers = headers;
508        self
509    }
510
511    /// Builds the [`FetchRequest`]
512    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/// FetchResult defines the resource response. Either a stream or buffered response are possible
529#[derive(Clone)]
530pub enum FetchResult {
531    /// Streamed response body
532    Stream {
533        /// Response metadata (status, headers, final URL)
534        meta: FetchResultMeta,
535        /// First bytes of the body, for content-type sniffing
536        peek_buf: PeekBuf,
537        /// Shared body that fans the stream out to all subscribers
538        shared: Arc<SharedBody>,
539    },
540    /// Buffered response body
541    Buffered {
542        /// Response metadata (status, headers, final URL)
543        meta: FetchResultMeta,
544        /// Complete response body
545        body: Bytes,
546    },
547    /// Network error occurred
548    Error(NetError),
549}
550
551impl FetchResult {
552    /// Returns true when the result is an error
553    pub fn is_error(&self) -> bool {
554        matches!(self, FetchResult::Error(_))
555    }
556
557    /// Return the metadata if available
558    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}