Skip to main content

nano_get/
client.rs

1use std::collections::HashMap;
2use std::io::BufReader;
3#[cfg(feature = "https")]
4use std::io::{Cursor, Read, Write};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, SystemTime};
7
8use crate::auth::{
9    parse_authenticate_headers, AuthDecision, AuthHandler, AuthTarget, BasicAuthHandler, Challenge,
10    DynAuthHandler,
11};
12use crate::date::parse_http_date;
13use crate::errors::NanoGetError;
14use crate::http::{self, BoxStream};
15#[cfg(feature = "https")]
16use crate::https;
17use crate::request::{should_follow_redirect, Header, Method, RedirectPolicy, Request};
18use crate::response::Response;
19use crate::url::{ToUrl, Url};
20
21/// Controls whether requests use one-off sockets or reusable persistent connections.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ConnectionPolicy {
24    /// Open a fresh connection per request and close it after each response.
25    Close,
26    /// Reuse compatible persistent connections when possible.
27    Reuse,
28}
29
30/// Controls whether the built-in in-memory cache is used.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CacheMode {
33    /// Disable caching entirely.
34    Disabled,
35    /// Enable the built-in process-local memory cache.
36    Memory,
37}
38
39/// Controls how strictly incoming HTTP/1.1 responses are parsed.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ParserStrictness {
42    /// Fail closed on ambiguous or unsafe wire framing.
43    Strict,
44    /// Accept a compatibility-oriented subset of non-strict server behavior.
45    Lenient,
46}
47
48impl ParserStrictness {
49    const fn is_strict(self) -> bool {
50        matches!(self, Self::Strict)
51    }
52}
53
54/// Proxy configuration for routing requests through an HTTP proxy.
55///
56/// The proxy URL itself must be `http://...`, including when tunneling HTTPS via `CONNECT`.
57#[derive(Debug, Clone)]
58pub struct ProxyConfig {
59    url: Url,
60    headers: Vec<Header>,
61}
62
63impl ProxyConfig {
64    /// Creates a new proxy configuration.
65    ///
66    /// The URL must use `http://`; otherwise [`NanoGetError::UnsupportedProxyScheme`] is
67    /// returned.
68    pub fn new<U: ToUrl>(url: U) -> Result<Self, NanoGetError> {
69        let url = url.to_url()?;
70        if !url.is_http() {
71            return Err(NanoGetError::UnsupportedProxyScheme(url.scheme.clone()));
72        }
73
74        Ok(Self {
75            url,
76            headers: Vec::new(),
77        })
78    }
79
80    /// Returns the parsed proxy URL.
81    pub fn url(&self) -> &Url {
82        &self.url
83    }
84
85    /// Returns additional headers that will be attached to proxy requests.
86    pub fn headers(&self) -> &[Header] {
87        &self.headers
88    }
89
90    /// Adds a custom header to outgoing proxy requests.
91    ///
92    /// Protocol-managed and hop-by-hop headers are rejected.
93    pub fn add_header(
94        &mut self,
95        name: impl Into<String>,
96        value: impl Into<String>,
97    ) -> Result<&mut Self, NanoGetError> {
98        let name = name.into();
99        validate_proxy_header_name(&name)?;
100        self.headers.push(Header::new(name, value)?);
101        Ok(self)
102    }
103}
104
105/// Builder for configuring a [`Client`].
106#[derive(Clone)]
107pub struct ClientBuilder {
108    config: ClientConfig,
109}
110
111impl ClientBuilder {
112    /// Creates a new builder with release-default settings.
113    pub fn new() -> Self {
114        Self {
115            config: ClientConfig::default(),
116        }
117    }
118
119    /// Sets the default redirect policy used by `Client::get`, `Client::head`, and requests
120    /// that do not override redirect handling explicitly.
121    pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self {
122        self.config.redirect_policy = policy;
123        self
124    }
125
126    /// Configures whether sessions should close each connection after one request or reuse
127    /// compatible connections for sequential and pipelined traffic.
128    pub fn connection_policy(mut self, policy: ConnectionPolicy) -> Self {
129        self.config.connection_policy = policy;
130        self
131    }
132
133    /// Enables or disables the built-in in-memory cache.
134    pub fn cache_mode(mut self, mode: CacheMode) -> Self {
135        self.config.cache_mode = mode;
136        self
137    }
138
139    /// Configures how strictly response framing and line syntax are validated.
140    ///
141    /// The default is [`ParserStrictness::Strict`].
142    pub fn parser_strictness(mut self, strictness: ParserStrictness) -> Self {
143        self.config.parser_strictness = strictness;
144        self
145    }
146
147    /// Routes requests through an explicit HTTP proxy.
148    pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
149        self.config.proxy = Some(proxy);
150        self
151    }
152
153    /// Installs a generic origin-authentication handler for `401` challenges.
154    pub fn auth_handler(mut self, handler: Arc<dyn AuthHandler + Send + Sync>) -> Self {
155        self.config.auth_handler = Some(handler);
156        self
157    }
158
159    /// Installs a generic proxy-authentication handler for `407` challenges.
160    pub fn proxy_auth_handler(mut self, handler: Arc<dyn AuthHandler + Send + Sync>) -> Self {
161        self.config.proxy_auth_handler = Some(handler);
162        self
163    }
164
165    /// Enables challenge-driven HTTP Basic authentication for origin servers.
166    pub fn basic_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
167        let handler = Arc::new(BasicAuthHandler::new(
168            username.into(),
169            password.into(),
170            AuthTarget::Origin,
171        ));
172        self.config.auth_handler = Some(handler);
173        self
174    }
175
176    /// Enables challenge-driven HTTP Basic authentication for the configured proxy.
177    pub fn basic_proxy_auth(
178        mut self,
179        username: impl Into<String>,
180        password: impl Into<String>,
181    ) -> Self {
182        let handler = Arc::new(BasicAuthHandler::new(
183            username.into(),
184            password.into(),
185            AuthTarget::Proxy,
186        ));
187        self.config.proxy_auth_handler = Some(handler);
188        self
189    }
190
191    /// Sends HTTP Basic origin credentials on the first request and still handles later `401`
192    /// challenges with the same credentials.
193    pub fn preemptive_basic_auth(
194        mut self,
195        username: impl Into<String>,
196        password: impl Into<String>,
197    ) -> Self {
198        let handler = BasicAuthHandler::new(username.into(), password.into(), AuthTarget::Origin);
199        self.config.preemptive_authorization = Some(handler.header_value().to_string());
200        self.config.auth_handler = Some(Arc::new(handler));
201        self
202    }
203
204    /// Sends HTTP Basic proxy credentials on the first proxy request or `CONNECT` attempt and
205    /// still handles later `407` challenges with the same credentials.
206    pub fn preemptive_basic_proxy_auth(
207        mut self,
208        username: impl Into<String>,
209        password: impl Into<String>,
210    ) -> Self {
211        let handler = BasicAuthHandler::new(username.into(), password.into(), AuthTarget::Proxy);
212        self.config.preemptive_proxy_authorization = Some(handler.header_value().to_string());
213        self.config.proxy_auth_handler = Some(Arc::new(handler));
214        self
215    }
216
217    /// Builds a [`Client`] from the current builder configuration.
218    pub fn build(self) -> Client {
219        Client {
220            inner: Arc::new(ClientInner {
221                config: self.config,
222                cache: Arc::new(Mutex::new(MemoryCache::default())),
223            }),
224        }
225    }
226}
227
228impl Default for ClientBuilder {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234/// Reusable synchronous HTTP client.
235///
236/// Use this when you want to configure redirects, connection reuse, caching, proxy behavior,
237/// and authentication once, then execute many requests.
238#[derive(Clone)]
239pub struct Client {
240    inner: Arc<ClientInner>,
241}
242
243struct ClientInner {
244    config: ClientConfig,
245    cache: Arc<Mutex<MemoryCache>>,
246}
247
248impl Client {
249    /// Creates a new [`ClientBuilder`].
250    pub fn builder() -> ClientBuilder {
251        ClientBuilder::new()
252    }
253
254    /// Creates a [`Session`] that can reuse a live connection for sequential requests.
255    pub fn session(&self) -> Session {
256        Session {
257            config: self.inner.config.clone(),
258            cache: Arc::clone(&self.inner.cache),
259            connection: None,
260        }
261    }
262
263    /// Executes a fully configured [`Request`].
264    pub fn execute(&self, request: Request) -> Result<Response, NanoGetError> {
265        self.session().execute(request)
266    }
267
268    /// Executes a borrowed [`Request`] by cloning it internally.
269    pub fn execute_ref(&self, request: &Request) -> Result<Response, NanoGetError> {
270        self.execute(request.clone())
271    }
272
273    /// Performs a `GET` with the client's default redirect policy and returns UTF-8 text.
274    pub fn get<U: ToUrl>(&self, url: U) -> Result<String, NanoGetError> {
275        let request = Request::get(url)?.with_redirect_policy(self.inner.config.redirect_policy);
276        self.execute(request).and_then(Response::into_body_text)
277    }
278
279    /// Performs a `GET` with the client's default redirect policy and returns raw bytes.
280    pub fn get_bytes<U: ToUrl>(&self, url: U) -> Result<Vec<u8>, NanoGetError> {
281        let request = Request::get(url)?.with_redirect_policy(self.inner.config.redirect_policy);
282        self.execute(request).map(|response| response.body)
283    }
284
285    /// Performs a `HEAD` with the client's default redirect policy.
286    pub fn head<U: ToUrl>(&self, url: U) -> Result<Response, NanoGetError> {
287        let request = Request::head(url)?.with_redirect_policy(self.inner.config.redirect_policy);
288        self.execute(request)
289    }
290}
291
292impl Default for Client {
293    fn default() -> Self {
294        ClientBuilder::default().build()
295    }
296}
297
298/// Stateful request executor that can hold a persistent connection.
299///
300/// A session is useful when you want tighter control over connection reuse and pipelining than
301/// calling [`Client::execute`] repeatedly.
302pub struct Session {
303    config: ClientConfig,
304    cache: Arc<Mutex<MemoryCache>>,
305    connection: Option<LiveConnection>,
306}
307
308impl Session {
309    /// Executes a request, applying configured redirects, cache behavior, auth retries,
310    /// and proxy routing.
311    pub fn execute(&mut self, request: Request) -> Result<Response, NanoGetError> {
312        let redirect_policy = request.effective_redirect_policy(self.config.redirect_policy);
313        let mut current = request;
314        let mut followed = 0usize;
315
316        loop {
317            let response = self.execute_one(current.clone())?;
318
319            match redirect_policy {
320                RedirectPolicy::None => return Ok(response),
321                RedirectPolicy::Follow { max_redirects } => {
322                    if !should_follow_redirect(response.status_code) {
323                        return Ok(response);
324                    }
325
326                    let Some(location) = response.header("location") else {
327                        return Ok(response);
328                    };
329
330                    if followed >= max_redirects {
331                        return Err(NanoGetError::RedirectLimitExceeded(max_redirects));
332                    }
333
334                    let next_url = current.url().resolve(location)?;
335                    let same_authority = current.url().same_authority(&next_url);
336                    current = current.clone_with_url(next_url);
337                    if !same_authority {
338                        current.remove_headers_named("authorization");
339                        current.disable_preemptive_origin_auth();
340                    }
341                    followed += 1;
342                }
343            }
344        }
345    }
346
347    /// Executes a borrowed request by cloning it internally.
348    pub fn execute_ref(&mut self, request: &Request) -> Result<Response, NanoGetError> {
349        self.execute(request.clone())
350    }
351
352    /// Sends multiple requests in one HTTP/1.1 pipeline.
353    ///
354    /// Requirements:
355    /// - [`ConnectionPolicy::Reuse`] must be enabled
356    /// - all requests must target the same underlying connection
357    ///
358    /// On success, responses are returned in request order.
359    pub fn execute_pipelined(
360        &mut self,
361        requests: &[Request],
362    ) -> Result<Vec<Response>, NanoGetError> {
363        if requests.is_empty() {
364            return Ok(Vec::new());
365        }
366
367        for request in requests {
368            validate_request_conditionals(request)?;
369        }
370
371        if self.config.connection_policy == ConnectionPolicy::Close {
372            return Err(NanoGetError::Pipeline(
373                "pipelining requires ConnectionPolicy::Reuse".to_string(),
374            ));
375        }
376
377        let key = connection_key(&self.config.proxy, requests[0].url());
378        for request in requests {
379            if request.url().scheme == "https" && self.config.proxy.is_none() {
380                if connection_key(&self.config.proxy, request.url()) != key {
381                    return Err(NanoGetError::Pipeline(
382                        "all pipelined requests must target the same connection".to_string(),
383                    ));
384                }
385            } else if connection_key(&self.config.proxy, request.url()) != key {
386                return Err(NanoGetError::Pipeline(
387                    "all pipelined requests must share the same connection".to_string(),
388                ));
389            }
390        }
391
392        self.ensure_connection(&requests[0])?;
393        {
394            let connection = self
395                .connection
396                .as_mut()
397                .ok_or_else(|| NanoGetError::Pipeline("missing live connection".to_string()))?;
398
399            for request in requests {
400                let send_target = SendTarget::for_request(&self.config, request);
401                let prepared = prepared_request(request, &self.config, send_target)?;
402                let target = request_target(&prepared, &self.config.proxy);
403                http::write_request(connection.reader.get_mut(), &prepared, &target, false)?;
404            }
405            connection.reader.get_mut().flush()?;
406        }
407
408        let mut responses = Vec::with_capacity(requests.len());
409        for (index, request) in requests.iter().enumerate() {
410            let parsed = {
411                let connection = self
412                    .connection
413                    .as_mut()
414                    .ok_or_else(|| NanoGetError::Pipeline("missing live connection".to_string()))?;
415                crate::response::read_parsed_response(
416                    &mut connection.reader,
417                    request.method(),
418                    self.config.parser_strictness.is_strict(),
419                )
420            };
421            let parsed = match parsed {
422                Ok(parsed) => parsed,
423                Err(error) if pipeline_retryable_parse_error(&error) => {
424                    self.connection = None;
425                    for remaining in requests.iter().skip(index) {
426                        responses.push(self.execute_one(remaining.clone())?);
427                    }
428                    return Ok(responses);
429                }
430                Err(error) => return Err(error),
431            };
432            responses.push(parsed.response);
433            if parsed.connection_close && index + 1 != requests.len() {
434                self.connection = None;
435                for remaining in requests.iter().skip(index + 1) {
436                    responses.push(self.execute_one(remaining.clone())?);
437                }
438                return Ok(responses);
439            }
440            if parsed.connection_close {
441                self.connection = None;
442            }
443        }
444
445        Ok(responses)
446    }
447
448    fn execute_one(&mut self, request: Request) -> Result<Response, NanoGetError> {
449        validate_request_conditionals(&request)?;
450        let auth_context = effective_auth_context(&request, &self.config);
451        let cache_directives = CacheControl::from_headers(request.headers());
452        if self.config.cache_mode != CacheMode::Memory && cache_directives.only_if_cached {
453            return Ok(gateway_timeout_response());
454        }
455
456        let now = SystemTime::now();
457        let mut bypass_standard_cache = false;
458        if self.config.cache_mode == CacheMode::Memory
459            && request.method() == Method::Get
460            && request.has_header("range")
461            && !cache_directives.no_store
462        {
463            let range_lookup = self
464                .cache
465                .lock()
466                .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?
467                .lookup_range(&request, now, &auth_context);
468            match range_lookup {
469                Some(RangeCacheLookup::Hit(response)) => {
470                    return Ok(response_for_method(response, request.method()))
471                }
472                Some(RangeCacheLookup::UnsatisfiedOnlyIfCached) => {
473                    return Ok(gateway_timeout_response())
474                }
475                Some(RangeCacheLookup::IfRangeMismatch) => {
476                    bypass_standard_cache = true;
477                }
478                None => {}
479            }
480        }
481
482        let cache_lookup = if !bypass_standard_cache
483            && self.config.cache_mode == CacheMode::Memory
484            && request.method() == Method::Get
485            && !cache_directives.no_store
486        {
487            self.cache
488                .lock()
489                .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?
490                .lookup(&request, now, &auth_context)
491        } else {
492            None
493        };
494
495        match cache_lookup {
496            Some(CacheLookup::Fresh(response)) => {
497                return Ok(response_for_method(response, request.method()))
498            }
499            Some(CacheLookup::Stale(entry)) => {
500                let revalidation = self.execute_stale(request, *entry)?;
501                return Ok(revalidation);
502            }
503            Some(CacheLookup::UnsatisfiedOnlyIfCached) => return Ok(gateway_timeout_response()),
504            None => {}
505        }
506
507        let mut current = request;
508        let mut seen_origin_challenges = Vec::new();
509        let mut seen_proxy_challenges = Vec::new();
510
511        loop {
512            let timed_response = self.send_request(&current)?;
513            let response = timed_response.response.clone();
514
515            if response.status_code == 401 {
516                if let Some(next_request) = self.maybe_retry_auth(
517                    AuthTarget::Origin,
518                    &current,
519                    &response,
520                    &mut seen_origin_challenges,
521                )? {
522                    current = next_request;
523                    continue;
524                }
525            } else if response.status_code == 407 {
526                if let Some(next_request) = self.maybe_retry_auth(
527                    AuthTarget::Proxy,
528                    &current,
529                    &response,
530                    &mut seen_proxy_challenges,
531                )? {
532                    current = next_request;
533                    continue;
534                }
535            }
536
537            let final_auth_context = effective_auth_context(&current, &self.config);
538            self.store_in_cache(&current, &timed_response, &final_auth_context)?;
539            return Ok(response);
540        }
541    }
542
543    fn execute_stale(
544        &mut self,
545        request: Request,
546        entry: CacheEntry,
547    ) -> Result<Response, NanoGetError> {
548        let mut conditional_request = request.clone();
549        if !has_user_conditionals(&conditional_request) {
550            if let Some(etag) = &entry.etag {
551                conditional_request.if_none_match(etag.clone())?;
552            } else if let Some(last_modified) = &entry.last_modified {
553                conditional_request.set_header("If-Modified-Since", last_modified.clone())?;
554            }
555        }
556
557        let response = self.send_request(&conditional_request)?.response;
558        if response.status_code == 304 {
559            let merged = {
560                let mut cache = self
561                    .cache
562                    .lock()
563                    .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?;
564                cache.merge_not_modified(&request, &entry, &response, SystemTime::now())?
565            };
566            return Ok(response_for_method(merged, request.method()));
567        }
568
569        let timed_response = TimedResponse::synthetic(response.clone());
570        let auth_context = effective_auth_context(&request, &self.config);
571        self.store_in_cache(&request, &timed_response, &auth_context)?;
572        Ok(response)
573    }
574
575    fn send_request(&mut self, request: &Request) -> Result<TimedResponse, NanoGetError> {
576        let should_reuse = self.config.connection_policy == ConnectionPolicy::Reuse;
577        let mut retried = false;
578
579        loop {
580            let result = if should_reuse {
581                self.ensure_connection(request)?;
582                self.send_on_live_connection(request)
583            } else {
584                self.send_ephemeral(request)
585            };
586
587            match result {
588                Ok(response) => return Ok(response),
589                Err(error) if should_reuse && !retried => {
590                    self.connection = None;
591                    retried = true;
592                    if retryable_reconnect_error(&error) {
593                        continue;
594                    }
595                    return Err(error);
596                }
597                Err(error) => return Err(error),
598            }
599        }
600    }
601
602    fn send_ephemeral(&self, request: &Request) -> Result<TimedResponse, NanoGetError> {
603        let mut connection = open_connection(&self.config, request)?;
604        let prepared = prepared_request(
605            request,
606            &self.config,
607            SendTarget::for_request(&self.config, request),
608        )?;
609        let target = request_target(&prepared, &self.config.proxy);
610
611        let request_time = SystemTime::now();
612        http::write_request(connection.reader.get_mut(), &prepared, &target, true)?;
613        connection.reader.get_mut().flush()?;
614        let parsed = crate::response::read_parsed_response(
615            &mut connection.reader,
616            request.method(),
617            self.config.parser_strictness.is_strict(),
618        )?;
619        let response_time = SystemTime::now();
620        Ok(TimedResponse {
621            response: parsed.response,
622            request_time,
623            response_time,
624        })
625    }
626
627    fn send_on_live_connection(
628        &mut self,
629        request: &Request,
630    ) -> Result<TimedResponse, NanoGetError> {
631        let send_target = SendTarget::for_request(&self.config, request);
632        let prepared = prepared_request(request, &self.config, send_target)?;
633        let target = request_target(&prepared, &self.config.proxy);
634        let connection = self.connection.as_mut().ok_or_else(|| {
635            NanoGetError::Io(std::io::Error::new(
636                std::io::ErrorKind::NotConnected,
637                "missing persistent connection",
638            ))
639        })?;
640
641        let request_time = SystemTime::now();
642        http::write_request(connection.reader.get_mut(), &prepared, &target, false)?;
643        connection.reader.get_mut().flush()?;
644        let parsed = crate::response::read_parsed_response(
645            &mut connection.reader,
646            request.method(),
647            self.config.parser_strictness.is_strict(),
648        )?;
649        let response_time = SystemTime::now();
650        let response = parsed.response;
651        let unexpected_head_body_bytes = request.method() == Method::Head
652            && !parsed.connection_close
653            && !connection.reader.buffer().is_empty();
654        if parsed.connection_close || unexpected_head_body_bytes {
655            self.connection = None;
656        }
657        Ok(TimedResponse {
658            response,
659            request_time,
660            response_time,
661        })
662    }
663
664    fn ensure_connection(&mut self, request: &Request) -> Result<(), NanoGetError> {
665        let desired = connection_key(&self.config.proxy, request.url());
666        let keep_existing = self
667            .connection
668            .as_ref()
669            .map(|connection| connection.key == desired)
670            .unwrap_or(false);
671        if keep_existing {
672            return Ok(());
673        }
674
675        self.connection = Some(open_connection(&self.config, request)?);
676        Ok(())
677    }
678
679    fn store_in_cache(
680        &self,
681        request: &Request,
682        timed_response: &TimedResponse,
683        auth_context: &AuthContext,
684    ) -> Result<(), NanoGetError> {
685        if self.config.cache_mode != CacheMode::Memory
686            || !matches!(request.method(), Method::Get | Method::Head)
687            || CacheControl::from_headers(request.headers()).no_store
688        {
689            return Ok(());
690        }
691
692        let mut cache = self
693            .cache
694            .lock()
695            .map_err(|_| NanoGetError::Cache("cache lock poisoned".to_string()))?;
696        cache.store(request, timed_response, auth_context);
697        Ok(())
698    }
699
700    fn maybe_retry_auth(
701        &self,
702        target: AuthTarget,
703        request: &Request,
704        response: &Response,
705        seen_challenges: &mut Vec<Vec<Challenge>>,
706    ) -> Result<Option<Request>, NanoGetError> {
707        let handler = match target {
708            AuthTarget::Origin => self.config.auth_handler.as_ref(),
709            AuthTarget::Proxy => self.config.proxy_auth_handler.as_ref(),
710        };
711        maybe_retry_request_auth(handler, target, request, response, seen_challenges)
712    }
713}
714
715#[derive(Clone)]
716struct ClientConfig {
717    redirect_policy: RedirectPolicy,
718    connection_policy: ConnectionPolicy,
719    cache_mode: CacheMode,
720    parser_strictness: ParserStrictness,
721    proxy: Option<ProxyConfig>,
722    auth_handler: Option<DynAuthHandler>,
723    proxy_auth_handler: Option<DynAuthHandler>,
724    preemptive_authorization: Option<String>,
725    preemptive_proxy_authorization: Option<String>,
726}
727
728impl Default for ClientConfig {
729    fn default() -> Self {
730        Self {
731            redirect_policy: RedirectPolicy::none(),
732            connection_policy: ConnectionPolicy::Close,
733            cache_mode: CacheMode::Disabled,
734            parser_strictness: ParserStrictness::Strict,
735            proxy: None,
736            auth_handler: None,
737            proxy_auth_handler: None,
738            preemptive_authorization: None,
739            preemptive_proxy_authorization: None,
740        }
741    }
742}
743
744struct LiveConnection {
745    key: ConnectionKey,
746    reader: BufReader<BoxStream>,
747}
748
749#[cfg(feature = "https")]
750struct PrefixedStream<S> {
751    prefix: Cursor<Vec<u8>>,
752    stream: S,
753}
754
755#[cfg(feature = "https")]
756impl<S> PrefixedStream<S> {
757    fn new(stream: S, prefix: Vec<u8>) -> Self {
758        Self {
759            prefix: Cursor::new(prefix),
760            stream,
761        }
762    }
763}
764
765#[cfg(feature = "https")]
766impl<S: Read> Read for PrefixedStream<S> {
767    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
768        let mut total = 0usize;
769
770        if (self.prefix.position() as usize) < self.prefix.get_ref().len() {
771            total += self.prefix.read(buf)?;
772            if total == buf.len() {
773                return Ok(total);
774            }
775        }
776
777        let read = self.stream.read(&mut buf[total..])?;
778        Ok(total + read)
779    }
780}
781
782#[cfg(feature = "https")]
783impl<S: Write> Write for PrefixedStream<S> {
784    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
785        self.stream.write(buf)
786    }
787
788    fn flush(&mut self) -> std::io::Result<()> {
789        self.stream.flush()
790    }
791}
792
793#[derive(Debug, Clone)]
794struct TimedResponse {
795    response: Response,
796    request_time: SystemTime,
797    response_time: SystemTime,
798}
799
800impl TimedResponse {
801    fn synthetic(response: Response) -> Self {
802        let now = SystemTime::now();
803        Self {
804            response,
805            request_time: now,
806            response_time: now,
807        }
808    }
809}
810
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
812enum SendTarget {
813    Direct,
814    HttpProxy,
815    Tunnel,
816}
817
818impl SendTarget {
819    fn for_request(config: &ClientConfig, request: &Request) -> Self {
820        match config.proxy {
821            Some(_) if request.url().is_http() => Self::HttpProxy,
822            Some(_) => Self::Tunnel,
823            None => Self::Direct,
824        }
825    }
826
827    fn uses_proxy(self) -> bool {
828        matches!(self, Self::HttpProxy)
829    }
830
831    fn allows_origin_auth(self) -> bool {
832        true
833    }
834
835    fn allows_proxy_auth(self) -> bool {
836        matches!(self, Self::HttpProxy)
837    }
838}
839
840#[derive(Debug, Clone, PartialEq, Eq, Default)]
841struct AuthContext {
842    origin: Option<String>,
843    proxy: Option<String>,
844}
845
846#[derive(Debug, Clone, PartialEq, Eq)]
847enum ConnectionKey {
848    Direct { scheme: String, authority: String },
849    HttpProxy { proxy: String },
850    HttpsTunnel { proxy: String, target: String },
851}
852
853fn connection_key(proxy: &Option<ProxyConfig>, url: &Url) -> ConnectionKey {
854    match proxy {
855        Some(proxy) if url.is_http() => ConnectionKey::HttpProxy {
856            proxy: proxy.url().authority_form(),
857        },
858        Some(proxy) => ConnectionKey::HttpsTunnel {
859            proxy: proxy.url().authority_form(),
860            target: url.authority_form(),
861        },
862        None => ConnectionKey::Direct {
863            scheme: url.scheme.clone(),
864            authority: url.authority_form(),
865        },
866    }
867}
868
869fn prepared_request(
870    request: &Request,
871    config: &ClientConfig,
872    send_target: SendTarget,
873) -> Result<Request, NanoGetError> {
874    let mut prepared = request.clone();
875
876    if !send_target.allows_proxy_auth() {
877        prepared.remove_headers_named("proxy-authorization");
878    }
879
880    if send_target.uses_proxy() {
881        for header in config.proxy.iter().flat_map(|proxy| proxy.headers()) {
882            if prepared.has_header(header.name()) {
883                continue;
884            }
885            prepared.add_header(header.name().to_string(), header.value().to_string())?;
886        }
887    }
888    if send_target.allows_origin_auth()
889        && prepared.preemptive_origin_auth_allowed()
890        && !prepared.has_header("authorization")
891    {
892        if let Some(value) = &config.preemptive_authorization {
893            prepared.authorization(value.clone())?;
894        }
895    }
896
897    if send_target.allows_proxy_auth() && !prepared.has_header("proxy-authorization") {
898        if let Some(value) = &config.preemptive_proxy_authorization {
899            prepared.proxy_authorization(value.clone())?;
900        }
901    }
902
903    Ok(prepared)
904}
905
906fn request_target(request: &Request, proxy: &Option<ProxyConfig>) -> String {
907    if proxy.is_some() && request.url().is_http() {
908        request.url().absolute_form()
909    } else {
910        request.url().origin_form()
911    }
912}
913
914fn open_connection(
915    config: &ClientConfig,
916    request: &Request,
917) -> Result<LiveConnection, NanoGetError> {
918    let key = connection_key(&config.proxy, request.url());
919    let stream = match &config.proxy {
920        Some(proxy) if request.url().is_http() => {
921            let stream = http::connect_tcp(&proxy.url().authority_form())?;
922            Box::new(stream) as BoxStream
923        }
924        Some(proxy) => open_https_tunnel(config, request, proxy)?,
925        None if request.url().is_http() => {
926            let stream = http::connect_tcp(&request.url().authority_form())?;
927            Box::new(stream) as BoxStream
928        }
929        None if request.url().is_https() => {
930            #[cfg(feature = "https")]
931            {
932                https::connect_tls(request.url())?
933            }
934            #[cfg(not(feature = "https"))]
935            {
936                return Err(NanoGetError::HttpsFeatureRequired);
937            }
938        }
939        None => {
940            return Err(NanoGetError::UnsupportedScheme(
941                request.url().scheme.clone(),
942            ))
943        }
944    };
945
946    Ok(LiveConnection {
947        key,
948        reader: BufReader::new(stream),
949    })
950}
951
952fn open_https_tunnel(
953    config: &ClientConfig,
954    request: &Request,
955    proxy: &ProxyConfig,
956) -> Result<BoxStream, NanoGetError> {
957    let mut current = request.clone();
958    let mut seen_proxy_challenges = Vec::new();
959
960    loop {
961        let mut stream = http::connect_tcp(&proxy.url().authority_form())?;
962        let connect_headers = prepared_connect_headers(&current, config, proxy)?;
963        let authority = request.url().authority_form();
964        http::write_connect_request(&mut stream, &authority, &connect_headers, false)?;
965        std::io::Write::flush(&mut stream)?;
966        let mut reader = BufReader::new(stream);
967        let head = http::read_response_head(&mut reader, config.parser_strictness.is_strict())?;
968        if (200..=299).contains(&head.status_code) {
969            let prefetched = reader.buffer().to_vec();
970            let stream = reader.into_inner();
971            #[cfg(feature = "https")]
972            {
973                if prefetched.is_empty() {
974                    return https::connect_tls_over_stream(request.url(), stream);
975                }
976                return https::connect_tls_over_stream(
977                    request.url(),
978                    PrefixedStream::new(stream, prefetched),
979                );
980            }
981            #[cfg(not(feature = "https"))]
982            {
983                let _ = prefetched;
984                let _ = stream;
985                return Err(NanoGetError::HttpsFeatureRequired);
986            }
987        }
988
989        if head.status_code != 407 {
990            break Err(NanoGetError::ProxyConnectFailed(
991                head.status_code,
992                head.reason_phrase,
993            ));
994        }
995        let response = Response {
996            version: head.version,
997            status_code: head.status_code,
998            reason_phrase: head.reason_phrase.clone(),
999            headers: head.headers,
1000            trailers: Vec::new(),
1001            body: Vec::new(),
1002        };
1003        let retry = maybe_retry_request_auth(
1004            config.proxy_auth_handler.as_ref(),
1005            AuthTarget::Proxy,
1006            &current,
1007            &response,
1008            &mut seen_proxy_challenges,
1009        )?;
1010        if let Some(retry) = retry {
1011            current = retry;
1012            continue;
1013        }
1014        break Err(NanoGetError::ProxyConnectFailed(
1015            response.status_code,
1016            response.reason_phrase,
1017        ));
1018    }
1019}
1020
1021#[derive(Default)]
1022struct MemoryCache {
1023    entries: HashMap<String, Vec<CacheEntry>>,
1024    partial_entries: HashMap<String, Vec<PartialCacheEntry>>,
1025}
1026
1027impl MemoryCache {
1028    fn lookup(
1029        &self,
1030        request: &Request,
1031        now: SystemTime,
1032        auth_context: &AuthContext,
1033    ) -> Option<CacheLookup> {
1034        let request_cache_control = CacheControl::from_headers(request.headers());
1035        let Some(entries) = self.entries.get(&request.url().cache_key()) else {
1036            return if request_cache_control.only_if_cached {
1037                Some(CacheLookup::UnsatisfiedOnlyIfCached)
1038            } else {
1039                None
1040            };
1041        };
1042        let Some(entry) = entries
1043            .iter()
1044            .filter(|entry| entry.matches(request, auth_context))
1045            .max_by_key(|entry| entry.response_time)
1046        else {
1047            return if request_cache_control.only_if_cached {
1048                Some(CacheLookup::UnsatisfiedOnlyIfCached)
1049            } else {
1050                None
1051            };
1052        };
1053
1054        if !entry.satisfies_request(&request_cache_control, now) {
1055            return if request_cache_control.only_if_cached {
1056                Some(CacheLookup::UnsatisfiedOnlyIfCached)
1057            } else {
1058                Some(CacheLookup::Stale(Box::new(entry.clone())))
1059            };
1060        }
1061
1062        Some(CacheLookup::Fresh(entry.response_with_age(now)))
1063    }
1064
1065    fn lookup_range(
1066        &self,
1067        request: &Request,
1068        now: SystemTime,
1069        auth_context: &AuthContext,
1070    ) -> Option<RangeCacheLookup> {
1071        let range_header = request.header("range")?;
1072        let range_spec = parse_single_range(range_header)?;
1073        let request_cache_control = CacheControl::from_headers(request.headers());
1074        let if_range = request.header("if-range").map(str::trim);
1075        let key = request.url().cache_key();
1076
1077        if let Some(entry) = self.entries.get(&key).and_then(|entries| {
1078            entries
1079                .iter()
1080                .filter(|entry| entry.matches(request, auth_context))
1081                .max_by_key(|entry| entry.response_time)
1082        }) {
1083            if !if_range_matches_entry(
1084                if_range,
1085                entry.etag.as_deref(),
1086                entry.last_modified.as_deref(),
1087            ) {
1088                return Some(if request_cache_control.only_if_cached {
1089                    RangeCacheLookup::UnsatisfiedOnlyIfCached
1090                } else {
1091                    RangeCacheLookup::IfRangeMismatch
1092                });
1093            }
1094            if entry.satisfies_request(&request_cache_control, now) {
1095                return Some(RangeCacheLookup::Hit(entry.range_response(range_spec, now)));
1096            }
1097        }
1098
1099        if let Some(entry) = self.partial_entries.get(&key).and_then(|entries| {
1100            entries
1101                .iter()
1102                .filter(|entry| entry.matches(request, auth_context))
1103                .max_by_key(|entry| entry.response_time)
1104        }) {
1105            if !if_range_matches_entry(
1106                if_range,
1107                Some(entry.etag.as_str()),
1108                entry.last_modified.as_deref(),
1109            ) {
1110                return Some(if request_cache_control.only_if_cached {
1111                    RangeCacheLookup::UnsatisfiedOnlyIfCached
1112                } else {
1113                    RangeCacheLookup::IfRangeMismatch
1114                });
1115            }
1116            let cached_range = entry
1117                .satisfies_request(&request_cache_control, now)
1118                .then(|| entry.range_response(range_spec, now))
1119                .flatten();
1120            if let Some(response) = cached_range {
1121                return Some(RangeCacheLookup::Hit(response));
1122            }
1123        }
1124
1125        if request_cache_control.only_if_cached {
1126            Some(RangeCacheLookup::UnsatisfiedOnlyIfCached)
1127        } else {
1128            None
1129        }
1130    }
1131
1132    fn store(&mut self, request: &Request, response: &TimedResponse, auth_context: &AuthContext) {
1133        if request.method() == Method::Get && response.response.status_code == 206 {
1134            self.store_partial(request, response, auth_context);
1135            return;
1136        }
1137
1138        let Some(entry) = CacheEntry::new(request, response, auth_context.clone()) else {
1139            return;
1140        };
1141
1142        let key = request.url().cache_key();
1143        let variants = self.entries.entry(key).or_default();
1144        if request.method() == Method::Head {
1145            if let Some(existing) = variants
1146                .iter_mut()
1147                .find(|existing| existing.same_variant(&entry))
1148            {
1149                if head_update_is_compatible(existing, &entry) {
1150                    let body = existing.response.body.clone();
1151                    let mut updated = entry;
1152                    updated.response.body = body;
1153                    *existing = updated;
1154                } else {
1155                    existing.freshness_lifetime = Duration::from_secs(0);
1156                    existing.cache_control.no_cache = true;
1157                }
1158            }
1159            return;
1160        }
1161
1162        if let Some(existing) = variants
1163            .iter_mut()
1164            .find(|existing| existing.same_variant(&entry))
1165        {
1166            *existing = entry;
1167            return;
1168        }
1169
1170        variants.push(entry);
1171    }
1172
1173    fn store_partial(
1174        &mut self,
1175        request: &Request,
1176        timed_response: &TimedResponse,
1177        auth_context: &AuthContext,
1178    ) {
1179        let Some(partial) = PartialCacheEntry::new(request, timed_response, auth_context.clone())
1180        else {
1181            return;
1182        };
1183
1184        let key = request.url().cache_key();
1185        let combined_entry = {
1186            let variants = self.partial_entries.entry(key.clone()).or_default();
1187            if let Some(existing) = variants
1188                .iter_mut()
1189                .find(|existing| existing.same_variant(&partial))
1190            {
1191                existing.merge_from(partial);
1192                existing.promote_complete()
1193            } else {
1194                let inserted = partial;
1195                let combined_entry = inserted.promote_complete();
1196                variants.push(inserted);
1197                combined_entry
1198            }
1199        };
1200
1201        if let Some(combined) = combined_entry {
1202            self.upsert_complete_entry(key, combined);
1203        }
1204    }
1205
1206    fn upsert_complete_entry(&mut self, key: String, entry: CacheEntry) {
1207        let variants = self.entries.entry(key).or_default();
1208        if let Some(existing) = variants
1209            .iter_mut()
1210            .find(|candidate| candidate.same_variant(&entry))
1211        {
1212            *existing = entry;
1213        } else {
1214            variants.push(entry);
1215        }
1216    }
1217
1218    fn merge_not_modified(
1219        &mut self,
1220        request: &Request,
1221        stale: &CacheEntry,
1222        not_modified: &Response,
1223        now: SystemTime,
1224    ) -> Result<Response, NanoGetError> {
1225        let variants = self
1226            .entries
1227            .get_mut(&request.url().cache_key())
1228            .ok_or_else(|| NanoGetError::Cache("stale cache entry disappeared".to_string()))?;
1229        let existing = variants
1230            .iter_mut()
1231            .find(|entry| entry.same_variant(stale))
1232            .ok_or_else(|| NanoGetError::Cache("stale cache variant disappeared".to_string()))?;
1233
1234        merge_headers_for_304(&mut existing.response.headers, &not_modified.headers);
1235        existing.cache_control = CacheControl::from_headers(&existing.response.headers);
1236        existing.request_time = now;
1237        existing.response_time = now;
1238        existing.freshness_lifetime = compute_freshness_lifetime(&existing.response, now);
1239        existing.age_header = parse_age_header(&existing.response.headers);
1240        existing.date_header = existing.response.header("date").and_then(parse_http_date);
1241        existing.etag = header_value(&existing.response.headers, "etag").map(str::to_string);
1242        existing.last_modified =
1243            header_value(&existing.response.headers, "last-modified").map(str::to_string);
1244        Ok(existing.response.clone())
1245    }
1246}
1247
1248enum RangeCacheLookup {
1249    Hit(Response),
1250    UnsatisfiedOnlyIfCached,
1251    IfRangeMismatch,
1252}
1253
1254#[derive(Clone, Copy)]
1255struct ByteRange {
1256    start: Option<usize>,
1257    end: Option<usize>,
1258}
1259
1260impl ByteRange {
1261    fn resolve(self, total_length: usize) -> Option<(usize, usize)> {
1262        if total_length == 0 {
1263            return None;
1264        }
1265
1266        match (self.start, self.end) {
1267            (Some(start), Some(end)) if start <= end && start < total_length => {
1268                Some((start, end.min(total_length - 1)))
1269            }
1270            (Some(start), None) if start < total_length => Some((start, total_length - 1)),
1271            (None, Some(suffix_len)) if suffix_len > 0 => {
1272                let len = suffix_len.min(total_length);
1273                Some((total_length - len, total_length - 1))
1274            }
1275            _ => None,
1276        }
1277    }
1278}
1279
1280#[derive(Clone)]
1281struct CacheEntry {
1282    vary_headers: Vec<VaryHeader>,
1283    response: Response,
1284    request_time: SystemTime,
1285    response_time: SystemTime,
1286    freshness_lifetime: Duration,
1287    cache_control: CacheControl,
1288    etag: Option<String>,
1289    last_modified: Option<String>,
1290    age_header: Option<Duration>,
1291    date_header: Option<SystemTime>,
1292    auth_context: AuthContext,
1293}
1294
1295impl CacheEntry {
1296    fn new(
1297        request: &Request,
1298        timed_response: &TimedResponse,
1299        auth_context: AuthContext,
1300    ) -> Option<Self> {
1301        let response = &timed_response.response;
1302        let cache_control = CacheControl::from_headers(&response.headers);
1303        if cache_control.no_store || !is_cacheable_status(response.status_code) {
1304            return None;
1305        }
1306        if auth_context.proxy.is_some() {
1307            return None;
1308        }
1309        if auth_context.origin.is_some() && !(cache_control.public || cache_control.private) {
1310            return None;
1311        }
1312
1313        let vary_headers = extract_vary_headers(request, response)?;
1314        Some(Self {
1315            vary_headers,
1316            response: response.clone(),
1317            request_time: timed_response.request_time,
1318            response_time: timed_response.response_time,
1319            freshness_lifetime: compute_freshness_lifetime(response, timed_response.response_time),
1320            cache_control,
1321            etag: response.header("etag").map(str::to_string),
1322            last_modified: response.header("last-modified").map(str::to_string),
1323            age_header: parse_age_header(&response.headers),
1324            date_header: response.header("date").and_then(parse_http_date),
1325            auth_context,
1326        })
1327    }
1328
1329    fn matches(&self, request: &Request, auth_context: &AuthContext) -> bool {
1330        self.auth_context == *auth_context
1331            && self.vary_headers.iter().all(|vary| vary.matches(request))
1332    }
1333
1334    fn same_variant(&self, other: &Self) -> bool {
1335        self.vary_headers == other.vary_headers
1336    }
1337
1338    fn is_fresh(&self, now: SystemTime) -> bool {
1339        if self.cache_control.no_cache {
1340            return false;
1341        }
1342
1343        self.current_age(now) <= self.freshness_lifetime
1344    }
1345
1346    fn current_age(&self, now: SystemTime) -> Duration {
1347        let apparent_age = match self.date_header {
1348            Some(date) => self
1349                .response_time
1350                .duration_since(date)
1351                .unwrap_or_else(|_| Duration::from_secs(0)),
1352            None => Duration::from_secs(0),
1353        };
1354        let corrected_received_age = self
1355            .age_header
1356            .map(|age| age.max(apparent_age))
1357            .unwrap_or(apparent_age);
1358        let response_delay = self
1359            .response_time
1360            .duration_since(self.request_time)
1361            .unwrap_or_else(|_| Duration::from_secs(0));
1362        let corrected_initial_age = corrected_received_age + response_delay;
1363        let resident_time = now
1364            .duration_since(self.response_time)
1365            .unwrap_or_else(|_| Duration::from_secs(0));
1366        corrected_initial_age + resident_time
1367    }
1368
1369    fn remaining_freshness(&self, now: SystemTime) -> Duration {
1370        self.freshness_lifetime
1371            .saturating_sub(self.current_age(now))
1372    }
1373
1374    fn staleness(&self, now: SystemTime) -> Duration {
1375        self.current_age(now)
1376            .saturating_sub(self.freshness_lifetime)
1377    }
1378
1379    fn satisfies_request(&self, request_cache_control: &CacheControl, now: SystemTime) -> bool {
1380        if request_cache_control.no_cache || self.cache_control.no_cache {
1381            return false;
1382        }
1383
1384        let age = self.current_age(now);
1385        if request_cache_control
1386            .max_age
1387            .is_some_and(|max_age| age > Duration::from_secs(max_age))
1388        {
1389            return false;
1390        }
1391
1392        if request_cache_control
1393            .min_fresh
1394            .is_some_and(|min_fresh| self.remaining_freshness(now) < Duration::from_secs(min_fresh))
1395        {
1396            return false;
1397        }
1398
1399        if self.is_fresh(now) {
1400            return true;
1401        }
1402
1403        if self.cache_control.must_revalidate || self.cache_control.proxy_revalidate {
1404            return false;
1405        }
1406
1407        match request_cache_control.max_stale {
1408            Some(None) => true,
1409            Some(Some(max_stale)) => self.staleness(now) <= Duration::from_secs(max_stale),
1410            None => false,
1411        }
1412    }
1413
1414    fn response_with_age(&self, now: SystemTime) -> Response {
1415        let mut response = self.response.clone();
1416        set_age_header(&mut response.headers, self.current_age(now));
1417        response
1418    }
1419
1420    fn range_response(&self, range: ByteRange, now: SystemTime) -> Response {
1421        match range.resolve(self.response.body.len()) {
1422            Some((start, end)) => {
1423                let mut response = self.response_with_age(now);
1424                response.status_code = 206;
1425                response.reason_phrase = "Partial Content".to_string();
1426                response.body = self.response.body[start..=end].to_vec();
1427                response
1428                    .headers
1429                    .retain(|header| !header.matches_name("content-range"));
1430                response
1431                    .headers
1432                    .retain(|header| !header.matches_name("content-length"));
1433                response.headers.push(Header::unchecked(
1434                    "Content-Range",
1435                    format!("bytes {start}-{end}/{}", self.response.body.len()),
1436                ));
1437                response.headers.push(Header::unchecked(
1438                    "Content-Length",
1439                    response.body.len().to_string(),
1440                ));
1441                response
1442            }
1443            None => {
1444                let mut response = self.response_with_age(now);
1445                response.status_code = 416;
1446                response.reason_phrase = "Range Not Satisfiable".to_string();
1447                response.body.clear();
1448                response
1449                    .headers
1450                    .retain(|header| !header.matches_name("content-range"));
1451                response
1452                    .headers
1453                    .retain(|header| !header.matches_name("content-length"));
1454                response.headers.push(Header::unchecked(
1455                    "Content-Range",
1456                    format!("bytes */{}", self.response.body.len()),
1457                ));
1458                response
1459                    .headers
1460                    .push(Header::unchecked("Content-Length", "0".to_string()));
1461                response
1462            }
1463        }
1464    }
1465}
1466
1467enum CacheLookup {
1468    Fresh(Response),
1469    Stale(Box<CacheEntry>),
1470    UnsatisfiedOnlyIfCached,
1471}
1472
1473#[derive(Clone)]
1474struct PartialCacheEntry {
1475    vary_headers: Vec<VaryHeader>,
1476    response: Response,
1477    request_time: SystemTime,
1478    response_time: SystemTime,
1479    freshness_lifetime: Duration,
1480    cache_control: CacheControl,
1481    age_header: Option<Duration>,
1482    date_header: Option<SystemTime>,
1483    auth_context: AuthContext,
1484    etag: String,
1485    last_modified: Option<String>,
1486    total_length: usize,
1487    segments: Vec<ByteSegment>,
1488}
1489
1490#[derive(Clone)]
1491struct ByteSegment {
1492    start: usize,
1493    end: usize,
1494    bytes: Vec<u8>,
1495}
1496
1497impl PartialCacheEntry {
1498    fn new(
1499        request: &Request,
1500        timed_response: &TimedResponse,
1501        auth_context: AuthContext,
1502    ) -> Option<Self> {
1503        let response = &timed_response.response;
1504        let cache_control = CacheControl::from_headers(&response.headers);
1505        if cache_control.no_store || response.status_code != 206 {
1506            return None;
1507        }
1508        if auth_context.proxy.is_some() {
1509            return None;
1510        }
1511        if auth_context.origin.is_some() && !(cache_control.public || cache_control.private) {
1512            return None;
1513        }
1514        let content_range = response.header("content-range")?;
1515        let (start, end, total_length) = parse_content_range(content_range)?;
1516        if end < start || end.saturating_sub(start) + 1 != response.body.len() {
1517            return None;
1518        }
1519        let etag = response.header("etag")?.trim().to_string();
1520        if !is_strong_etag(&etag) {
1521            return None;
1522        }
1523        let vary_headers = extract_vary_headers(request, response)?;
1524        Some(Self {
1525            vary_headers,
1526            response: response.clone(),
1527            request_time: timed_response.request_time,
1528            response_time: timed_response.response_time,
1529            freshness_lifetime: compute_freshness_lifetime(response, timed_response.response_time),
1530            cache_control,
1531            age_header: parse_age_header(&response.headers),
1532            date_header: response.header("date").and_then(parse_http_date),
1533            auth_context,
1534            etag,
1535            last_modified: response.header("last-modified").map(str::to_string),
1536            total_length,
1537            segments: vec![ByteSegment {
1538                start,
1539                end,
1540                bytes: response.body.clone(),
1541            }],
1542        })
1543    }
1544
1545    fn matches(&self, request: &Request, auth_context: &AuthContext) -> bool {
1546        self.auth_context == *auth_context
1547            && self.vary_headers.iter().all(|vary| vary.matches(request))
1548    }
1549
1550    fn same_variant(&self, other: &Self) -> bool {
1551        self.vary_headers == other.vary_headers
1552            && self.auth_context == other.auth_context
1553            && self.etag == other.etag
1554            && self.total_length == other.total_length
1555    }
1556
1557    fn merge_from(&mut self, mut other: Self) {
1558        self.request_time = other.request_time;
1559        self.response_time = other.response_time;
1560        self.freshness_lifetime = other.freshness_lifetime;
1561        self.cache_control = other.cache_control;
1562        self.age_header = other.age_header;
1563        self.date_header = other.date_header;
1564        self.last_modified = other.last_modified.take();
1565        self.response = other.response;
1566        self.segments.append(&mut other.segments);
1567        normalize_segments(&mut self.segments);
1568    }
1569
1570    fn current_age(&self, now: SystemTime) -> Duration {
1571        let apparent_age = match self.date_header {
1572            Some(date) => self
1573                .response_time
1574                .duration_since(date)
1575                .unwrap_or_else(|_| Duration::from_secs(0)),
1576            None => Duration::from_secs(0),
1577        };
1578        let corrected_received_age = self
1579            .age_header
1580            .map(|age| age.max(apparent_age))
1581            .unwrap_or(apparent_age);
1582        let response_delay = self
1583            .response_time
1584            .duration_since(self.request_time)
1585            .unwrap_or_else(|_| Duration::from_secs(0));
1586        let corrected_initial_age = corrected_received_age + response_delay;
1587        let resident_time = now
1588            .duration_since(self.response_time)
1589            .unwrap_or_else(|_| Duration::from_secs(0));
1590        corrected_initial_age + resident_time
1591    }
1592
1593    fn remaining_freshness(&self, now: SystemTime) -> Duration {
1594        self.freshness_lifetime
1595            .saturating_sub(self.current_age(now))
1596    }
1597
1598    fn staleness(&self, now: SystemTime) -> Duration {
1599        self.current_age(now)
1600            .saturating_sub(self.freshness_lifetime)
1601    }
1602
1603    fn is_fresh(&self, now: SystemTime) -> bool {
1604        if self.cache_control.no_cache {
1605            return false;
1606        }
1607        self.current_age(now) <= self.freshness_lifetime
1608    }
1609
1610    fn satisfies_request(&self, request_cache_control: &CacheControl, now: SystemTime) -> bool {
1611        if request_cache_control.no_cache || self.cache_control.no_cache {
1612            return false;
1613        }
1614
1615        let age = self.current_age(now);
1616        if request_cache_control
1617            .max_age
1618            .is_some_and(|max_age| age > Duration::from_secs(max_age))
1619        {
1620            return false;
1621        }
1622
1623        if request_cache_control
1624            .min_fresh
1625            .is_some_and(|min_fresh| self.remaining_freshness(now) < Duration::from_secs(min_fresh))
1626        {
1627            return false;
1628        }
1629
1630        if self.is_fresh(now) {
1631            return true;
1632        }
1633
1634        if self.cache_control.must_revalidate || self.cache_control.proxy_revalidate {
1635            return false;
1636        }
1637
1638        match request_cache_control.max_stale {
1639            Some(None) => true,
1640            Some(Some(max_stale)) => self.staleness(now) <= Duration::from_secs(max_stale),
1641            None => false,
1642        }
1643    }
1644
1645    fn range_response(&self, range: ByteRange, now: SystemTime) -> Option<Response> {
1646        let (start, end) = range.resolve(self.total_length)?;
1647        let segment = self
1648            .segments
1649            .iter()
1650            .find(|segment| segment.start <= start && segment.end >= end)?;
1651        let offset_start = start - segment.start;
1652        let offset_end = end - segment.start + 1;
1653
1654        let mut response = self.response.clone();
1655        set_age_header(&mut response.headers, self.current_age(now));
1656        response.status_code = 206;
1657        response.reason_phrase = "Partial Content".to_string();
1658        response.body = segment.bytes[offset_start..offset_end].to_vec();
1659        response
1660            .headers
1661            .retain(|header| !header.matches_name("content-range"));
1662        response
1663            .headers
1664            .retain(|header| !header.matches_name("content-length"));
1665        response.headers.push(Header::unchecked(
1666            "Content-Range",
1667            format!("bytes {start}-{end}/{}", self.total_length),
1668        ));
1669        response.headers.push(Header::unchecked(
1670            "Content-Length",
1671            response.body.len().to_string(),
1672        ));
1673        Some(response)
1674    }
1675
1676    fn promote_complete(&self) -> Option<CacheEntry> {
1677        if self.segments.len() != 1 {
1678            return None;
1679        }
1680        let segment = &self.segments[0];
1681        if segment.start != 0 || segment.end + 1 != self.total_length {
1682            return None;
1683        }
1684
1685        let mut headers: Vec<Header> = self
1686            .response
1687            .headers
1688            .iter()
1689            .filter(|header| {
1690                !header.matches_name("content-range") && !header.matches_name("content-length")
1691            })
1692            .cloned()
1693            .collect();
1694        headers.push(Header::unchecked(
1695            "Content-Length",
1696            self.total_length.to_string(),
1697        ));
1698
1699        let response = Response {
1700            version: self.response.version,
1701            status_code: 200,
1702            reason_phrase: "OK".to_string(),
1703            headers,
1704            trailers: Vec::new(),
1705            body: segment.bytes.clone(),
1706        };
1707
1708        Some(CacheEntry {
1709            vary_headers: self.vary_headers.clone(),
1710            response,
1711            request_time: self.request_time,
1712            response_time: self.response_time,
1713            freshness_lifetime: self.freshness_lifetime,
1714            cache_control: self.cache_control,
1715            etag: Some(self.etag.clone()),
1716            last_modified: self.last_modified.clone(),
1717            age_header: self.age_header,
1718            date_header: self.date_header,
1719            auth_context: self.auth_context.clone(),
1720        })
1721    }
1722}
1723
1724#[derive(Debug, Clone, PartialEq, Eq)]
1725struct VaryHeader {
1726    name: String,
1727    values: Vec<String>,
1728}
1729
1730impl VaryHeader {
1731    fn matches(&self, request: &Request) -> bool {
1732        let mut values = request
1733            .headers_named(&self.name)
1734            .map(|header| header.value());
1735        for expected in &self.values {
1736            match values.next() {
1737                Some(value) if value == expected => {}
1738                _ => return false,
1739            }
1740        }
1741        values.next().is_none()
1742    }
1743}
1744
1745#[derive(Debug, Clone, Copy, Default)]
1746struct CacheControl {
1747    no_store: bool,
1748    no_cache: bool,
1749    max_age: Option<u64>,
1750    max_stale: Option<Option<u64>>,
1751    min_fresh: Option<u64>,
1752    only_if_cached: bool,
1753    must_revalidate: bool,
1754    proxy_revalidate: bool,
1755    public: bool,
1756    private: bool,
1757}
1758
1759impl CacheControl {
1760    fn from_headers(headers: &[Header]) -> Self {
1761        let mut directives = CacheControl::default();
1762        for value in headers
1763            .iter()
1764            .filter(|header| header.matches_name("cache-control"))
1765            .map(Header::value)
1766        {
1767            for directive in value
1768                .split(',')
1769                .map(str::trim)
1770                .filter(|value| !value.is_empty())
1771            {
1772                let (name, argument) = match directive.split_once('=') {
1773                    Some((name, argument)) => {
1774                        (name.trim().to_ascii_lowercase(), Some(argument.trim()))
1775                    }
1776                    None => (directive.to_ascii_lowercase(), None),
1777                };
1778                match name.as_str() {
1779                    "no-store" => directives.no_store = true,
1780                    "no-cache" => directives.no_cache = true,
1781                    "must-revalidate" => directives.must_revalidate = true,
1782                    "proxy-revalidate" => directives.proxy_revalidate = true,
1783                    "public" => directives.public = true,
1784                    "private" => directives.private = true,
1785                    "only-if-cached" => directives.only_if_cached = true,
1786                    "max-age" => {
1787                        directives.max_age = argument.and_then(parse_directive_u64);
1788                    }
1789                    "max-stale" if argument.is_none() => {
1790                        directives.max_stale = Some(None);
1791                    }
1792                    "max-stale" => {
1793                        directives.max_stale = argument.and_then(parse_directive_u64).map(Some);
1794                    }
1795                    "min-fresh" => {
1796                        directives.min_fresh = argument.and_then(parse_directive_u64);
1797                    }
1798                    _ => {}
1799                }
1800            }
1801        }
1802
1803        directives
1804    }
1805}
1806
1807fn response_for_method(mut response: Response, method: Method) -> Response {
1808    if method == Method::Head {
1809        response.body.clear();
1810    }
1811
1812    response
1813}
1814
1815fn pipeline_retryable_parse_error(error: &NanoGetError) -> bool {
1816    matches!(
1817        error,
1818        NanoGetError::Io(_) | NanoGetError::IncompleteMessage(_)
1819    )
1820}
1821
1822fn retryable_reconnect_error(error: &NanoGetError) -> bool {
1823    matches!(error, NanoGetError::Connect(_) | NanoGetError::Tls(_))
1824        || pipeline_retryable_parse_error(error)
1825}
1826
1827fn has_user_conditionals(request: &Request) -> bool {
1828    [
1829        "if-none-match",
1830        "if-match",
1831        "if-modified-since",
1832        "if-unmodified-since",
1833        "if-range",
1834    ]
1835    .iter()
1836    .any(|name| request.has_header(name))
1837}
1838
1839fn extract_vary_headers(request: &Request, response: &Response) -> Option<Vec<VaryHeader>> {
1840    let vary_names: Vec<String> = response
1841        .headers_named("vary")
1842        .flat_map(|header| header.value().split(','))
1843        .map(str::trim)
1844        .filter(|value| !value.is_empty())
1845        .map(str::to_ascii_lowercase)
1846        .collect();
1847    if vary_names.is_empty() {
1848        return Some(Vec::new());
1849    }
1850    if vary_names.iter().any(|value| value == "*") {
1851        return None;
1852    }
1853
1854    Some(
1855        vary_names
1856            .iter()
1857            .map(|name| VaryHeader {
1858                name: name.clone(),
1859                values: request
1860                    .headers_named(name)
1861                    .map(|header| header.value().to_string())
1862                    .collect(),
1863            })
1864            .collect(),
1865    )
1866}
1867
1868fn is_cacheable_status(status_code: u16) -> bool {
1869    matches!(
1870        status_code,
1871        200 | 203 | 204 | 300 | 301 | 404 | 405 | 410 | 414 | 501
1872    )
1873}
1874
1875fn compute_freshness_lifetime(response: &Response, now: SystemTime) -> Duration {
1876    let cache_control = CacheControl::from_headers(&response.headers);
1877    if let Some(max_age) = cache_control.max_age {
1878        return Duration::from_secs(max_age);
1879    }
1880
1881    if let Some(expires) = response.header("expires").and_then(parse_http_date) {
1882        if let Some(date) = response.header("date").and_then(parse_http_date) {
1883            return expires
1884                .duration_since(date)
1885                .unwrap_or_else(|_| Duration::from_secs(0));
1886        }
1887        return expires
1888            .duration_since(now)
1889            .unwrap_or_else(|_| Duration::from_secs(0));
1890    }
1891
1892    if let (Some(last_modified), Some(date)) = (
1893        response.header("last-modified").and_then(parse_http_date),
1894        response.header("date").and_then(parse_http_date),
1895    ) {
1896        return date
1897            .duration_since(last_modified)
1898            .map(|age| (age / 10).min(Duration::from_secs(86_400)))
1899            .unwrap_or(Duration::from_secs(0));
1900    }
1901
1902    Duration::from_secs(0)
1903}
1904
1905fn header_value<'a>(headers: &'a [Header], name: &str) -> Option<&'a str> {
1906    headers
1907        .iter()
1908        .find(|header| header.matches_name(name))
1909        .map(Header::value)
1910}
1911
1912fn merge_headers_for_304(stored: &mut Vec<Header>, fresh: &[Header]) {
1913    for header in fresh {
1914        if header.matches_name("content-length")
1915            || header.matches_name("transfer-encoding")
1916            || header.matches_name("content-range")
1917        {
1918            continue;
1919        }
1920        stored.retain(|existing| !existing.matches_name(header.name()));
1921        stored.push(header.clone());
1922    }
1923}
1924
1925fn parse_age_header(headers: &[Header]) -> Option<Duration> {
1926    header_value(headers, "age")
1927        .and_then(|value| value.trim().parse::<u64>().ok())
1928        .map(Duration::from_secs)
1929}
1930
1931fn set_age_header(headers: &mut Vec<Header>, age: Duration) {
1932    let age_seconds = age.as_secs().to_string();
1933    headers.retain(|header| !header.matches_name("age"));
1934    headers.push(Header::unchecked("Age", age_seconds));
1935}
1936
1937fn head_update_is_compatible(existing: &CacheEntry, candidate: &CacheEntry) -> bool {
1938    let current_etag = existing.etag.as_deref();
1939    let candidate_etag = candidate.etag.as_deref();
1940    if let (Some(current), Some(next)) = (current_etag, candidate_etag) {
1941        if current != next {
1942            return false;
1943        }
1944    }
1945
1946    if candidate
1947        .response
1948        .header("content-length")
1949        .and_then(|value| value.parse::<usize>().ok())
1950        .is_some_and(|content_length| content_length != existing.response.body.len())
1951    {
1952        return false;
1953    }
1954
1955    true
1956}
1957
1958fn parse_directive_u64(value: &str) -> Option<u64> {
1959    let value = value.trim().trim_matches('"');
1960    value.parse::<u64>().ok()
1961}
1962
1963fn parse_single_range(value: &str) -> Option<ByteRange> {
1964    let value = value.trim();
1965    let bytes = value.strip_prefix("bytes=")?.trim();
1966    if bytes.contains(',') {
1967        return None;
1968    }
1969    let (start, end) = bytes.split_once('-')?;
1970    if start.is_empty() {
1971        return Some(ByteRange {
1972            start: None,
1973            end: end.parse().ok(),
1974        });
1975    }
1976    let start_value = start.parse().ok()?;
1977    if end.is_empty() {
1978        return Some(ByteRange {
1979            start: Some(start_value),
1980            end: None,
1981        });
1982    }
1983    Some(ByteRange {
1984        start: Some(start_value),
1985        end: end.parse().ok(),
1986    })
1987}
1988
1989fn parse_content_range(value: &str) -> Option<(usize, usize, usize)> {
1990    let value = value.trim();
1991    let bytes = value.strip_prefix("bytes ")?;
1992    let (range, complete_length) = bytes.split_once('/')?;
1993    let complete_length = complete_length.parse().ok()?;
1994    let (start, end) = range.split_once('-')?;
1995    let start = start.parse().ok()?;
1996    let end = end.parse().ok()?;
1997    Some((start, end, complete_length))
1998}
1999
2000fn is_strong_etag(value: &str) -> bool {
2001    let value = value.trim();
2002    value.starts_with('"')
2003        && value.ends_with('"')
2004        && !value.starts_with("W/")
2005        && !value.starts_with("w/")
2006}
2007
2008fn if_range_matches_entry(
2009    if_range: Option<&str>,
2010    etag: Option<&str>,
2011    last_modified: Option<&str>,
2012) -> bool {
2013    let Some(if_range) = if_range else {
2014        return true;
2015    };
2016
2017    if if_range.starts_with('"') || if_range.starts_with("W/") || if_range.starts_with("w/") {
2018        return etag.is_some_and(|cached| cached == if_range);
2019    }
2020
2021    let Some(if_range_date) = parse_http_date(if_range) else {
2022        return false;
2023    };
2024    let Some(last_modified_date) = last_modified.and_then(parse_http_date) else {
2025        return false;
2026    };
2027    last_modified_date <= if_range_date
2028}
2029
2030fn validate_request_conditionals(request: &Request) -> Result<(), NanoGetError> {
2031    let Some(if_range) = request.header("if-range") else {
2032        return Ok(());
2033    };
2034    if !request.has_header("range") {
2035        return Err(NanoGetError::InvalidConditionalRequest(
2036            "If-Range requires Range".to_string(),
2037        ));
2038    }
2039    let if_range = if_range.trim();
2040    if if_range.starts_with("W/") || if_range.starts_with("w/") {
2041        return Err(NanoGetError::InvalidConditionalRequest(
2042            "weak ETags are not valid in If-Range".to_string(),
2043        ));
2044    }
2045    Ok(())
2046}
2047
2048fn normalize_segments(segments: &mut Vec<ByteSegment>) {
2049    if segments.is_empty() {
2050        return;
2051    }
2052    segments.sort_by_key(|segment| segment.start);
2053    let mut merged: Vec<ByteSegment> = Vec::with_capacity(segments.len());
2054
2055    for segment in segments.drain(..) {
2056        if let Some(last) = merged.last_mut() {
2057            if segment.start <= last.end.saturating_add(1) {
2058                let merged_start = last.start;
2059                let merged_end = last.end.max(segment.end);
2060                let mut bytes = vec![0u8; merged_end - merged_start + 1];
2061                let last_offset = last.start - merged_start;
2062                bytes[last_offset..last_offset + last.bytes.len()].copy_from_slice(&last.bytes);
2063                let segment_offset = segment.start - merged_start;
2064                bytes[segment_offset..segment_offset + segment.bytes.len()]
2065                    .copy_from_slice(&segment.bytes);
2066                last.start = merged_start;
2067                last.end = merged_end;
2068                last.bytes = bytes;
2069                continue;
2070            }
2071        }
2072        merged.push(segment);
2073    }
2074
2075    *segments = merged;
2076}
2077
2078fn validate_proxy_header_name(name: &str) -> Result<(), NanoGetError> {
2079    match name.to_ascii_lowercase().as_str() {
2080        "host" | "connection" | "content-length" | "transfer-encoding" | "trailer" | "upgrade" => {
2081            Err(NanoGetError::ProtocolManagedHeader(name.to_string()))
2082        }
2083        "keep-alive" | "proxy-connection" | "te" => {
2084            Err(NanoGetError::HopByHopHeader(name.to_string()))
2085        }
2086        _ => Ok(()),
2087    }
2088}
2089
2090fn effective_auth_context(request: &Request, config: &ClientConfig) -> AuthContext {
2091    AuthContext {
2092        origin: request
2093            .header("authorization")
2094            .map(str::to_string)
2095            .or_else(|| config.preemptive_authorization.clone()),
2096        proxy: request
2097            .header("proxy-authorization")
2098            .map(str::to_string)
2099            .or_else(|| config.preemptive_proxy_authorization.clone())
2100            .filter(|_| config.proxy.is_some()),
2101    }
2102}
2103
2104fn prepared_connect_headers(
2105    request: &Request,
2106    config: &ClientConfig,
2107    proxy: &ProxyConfig,
2108) -> Result<Vec<Header>, NanoGetError> {
2109    let mut headers: Vec<Header> = proxy.headers().to_vec();
2110    headers.retain(|header| !header.matches_name("authorization"));
2111
2112    if headers
2113        .iter()
2114        .any(|header| header.matches_name("proxy-authorization"))
2115    {
2116        return Ok(headers);
2117    }
2118
2119    let proxy_authorization = request
2120        .header("proxy-authorization")
2121        .map(str::to_string)
2122        .or_else(|| config.preemptive_proxy_authorization.clone());
2123    if let Some(value) = proxy_authorization {
2124        headers.push(Header::new("Proxy-Authorization", value)?);
2125    }
2126
2127    Ok(headers)
2128}
2129
2130fn maybe_retry_request_auth(
2131    handler: Option<&DynAuthHandler>,
2132    target: AuthTarget,
2133    request: &Request,
2134    response: &Response,
2135    seen_challenges: &mut Vec<Vec<Challenge>>,
2136) -> Result<Option<Request>, NanoGetError> {
2137    let header_name = match target {
2138        AuthTarget::Origin => "www-authenticate",
2139        AuthTarget::Proxy => "proxy-authenticate",
2140    };
2141    if !response
2142        .headers
2143        .iter()
2144        .any(|header| header.matches_name(header_name))
2145    {
2146        return Ok(None);
2147    }
2148
2149    let existing_header = match target {
2150        AuthTarget::Origin => "authorization",
2151        AuthTarget::Proxy => "proxy-authorization",
2152    };
2153    if request.has_header(existing_header) {
2154        return Ok(None);
2155    }
2156
2157    let challenges = parse_authenticate_headers(&response.headers, header_name)?;
2158    if challenges.is_empty() {
2159        return Ok(None);
2160    }
2161    if seen_challenges
2162        .iter()
2163        .any(|previous| previous == &challenges)
2164    {
2165        return Ok(None);
2166    }
2167    seen_challenges.push(challenges.clone());
2168
2169    let Some(handler) = handler else {
2170        return Ok(None);
2171    };
2172
2173    match handler.respond(target, request.url(), &challenges, request, response)? {
2174        AuthDecision::UseHeaders(headers) => {
2175            let mut retry = request.clone();
2176            retry.remove_headers_named(existing_header);
2177            for header in headers {
2178                if header.matches_name(existing_header) {
2179                    retry.set_header(header.name().to_string(), header.value().to_string())?;
2180                }
2181            }
2182            Ok(Some(retry))
2183        }
2184        AuthDecision::NoMatch => Ok(None),
2185        AuthDecision::Abort => Err(NanoGetError::AuthenticationRejected(match target {
2186            AuthTarget::Origin => "origin authentication handler aborted".to_string(),
2187            AuthTarget::Proxy => "proxy authentication handler aborted".to_string(),
2188        })),
2189    }
2190}
2191
2192fn gateway_timeout_response() -> Response {
2193    Response {
2194        version: crate::response::HttpVersion::Http11,
2195        status_code: 504,
2196        reason_phrase: "Gateway Timeout".to_string(),
2197        headers: Vec::new(),
2198        trailers: Vec::new(),
2199        body: Vec::new(),
2200    }
2201}
2202
2203#[cfg(test)]
2204mod tests {
2205    use std::io::{Read, Write};
2206    use std::net::TcpListener;
2207    use std::sync::{Arc, Mutex};
2208    use std::thread;
2209    use std::time::SystemTime;
2210
2211    use std::time::{Duration, UNIX_EPOCH};
2212
2213    use super::{
2214        if_range_matches_entry, is_strong_etag, normalize_segments, parse_content_range,
2215        parse_single_range, pipeline_retryable_parse_error, prepared_connect_headers,
2216        retryable_reconnect_error, validate_request_conditionals, AuthContext, ByteRange,
2217        ByteSegment, CacheControl, CacheEntry, CacheMode, Client, ClientBuilder, ClientConfig,
2218        ConnectionPolicy, MemoryCache, ParserStrictness, PartialCacheEntry, ProxyConfig,
2219        TimedResponse,
2220    };
2221    use crate::auth::{AuthDecision, AuthHandler, AuthTarget, Challenge};
2222    use crate::request::{Header, Method, RedirectPolicy, Request};
2223    use crate::response::{HttpVersion, Response};
2224    use crate::url::Url;
2225
2226    struct AbortAuthHandler;
2227
2228    impl AuthHandler for AbortAuthHandler {
2229        fn respond(
2230            &self,
2231            _target: AuthTarget,
2232            _url: &Url,
2233            _challenges: &[Challenge],
2234            _request: &Request,
2235            _response: &Response,
2236        ) -> Result<AuthDecision, crate::NanoGetError> {
2237            Ok(AuthDecision::Abort)
2238        }
2239    }
2240
2241    fn timed_response(response: Response) -> TimedResponse {
2242        let now = SystemTime::now();
2243        TimedResponse {
2244            response,
2245            request_time: now,
2246            response_time: now,
2247        }
2248    }
2249
2250    #[cfg(feature = "https")]
2251    #[test]
2252    fn prefixed_stream_reads_prefix_before_inner_stream() {
2253        let inner = std::io::Cursor::new(b"tail".to_vec());
2254        let mut stream = super::PrefixedStream::new(inner, b"head".to_vec());
2255        let mut body = Vec::new();
2256        stream.read_to_end(&mut body).unwrap();
2257        assert_eq!(body, b"headtail");
2258    }
2259
2260    fn response(status: u16, headers: Vec<crate::Header>, body: &[u8]) -> Response {
2261        Response {
2262            version: HttpVersion::Http11,
2263            status_code: status,
2264            reason_phrase: "OK".to_string(),
2265            headers,
2266            trailers: Vec::new(),
2267            body: body.to_vec(),
2268        }
2269    }
2270
2271    struct NoMatchAuthHandler;
2272
2273    impl AuthHandler for NoMatchAuthHandler {
2274        fn respond(
2275            &self,
2276            _target: AuthTarget,
2277            _url: &Url,
2278            _challenges: &[Challenge],
2279            _request: &Request,
2280            _response: &Response,
2281        ) -> Result<AuthDecision, crate::NanoGetError> {
2282            Ok(AuthDecision::NoMatch)
2283        }
2284    }
2285
2286    struct UseHeadersAuthHandler {
2287        header_name: &'static str,
2288        header_value: &'static str,
2289    }
2290
2291    impl AuthHandler for UseHeadersAuthHandler {
2292        fn respond(
2293            &self,
2294            _target: AuthTarget,
2295            _url: &Url,
2296            _challenges: &[Challenge],
2297            _request: &Request,
2298            _response: &Response,
2299        ) -> Result<AuthDecision, crate::NanoGetError> {
2300            Ok(AuthDecision::UseHeaders(vec![
2301                Header::unchecked("X-Ignored", "1"),
2302                Header::unchecked(self.header_name, self.header_value),
2303            ]))
2304        }
2305    }
2306
2307    fn spawn_recording_server(
2308        responses: Vec<Vec<u8>>,
2309    ) -> (u16, Arc<Mutex<Vec<String>>>, thread::JoinHandle<()>) {
2310        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2311        let port = listener.local_addr().unwrap().port();
2312        let requests = Arc::new(Mutex::new(Vec::<String>::new()));
2313        let requests_for_thread = Arc::clone(&requests);
2314        let handle = thread::spawn(move || {
2315            for response in responses {
2316                let (mut stream, _) = listener.accept().unwrap();
2317                let mut request = Vec::new();
2318                let mut chunk = [0u8; 256];
2319                let mut read = stream.read(&mut chunk).unwrap();
2320                while read > 0 {
2321                    request.extend_from_slice(&chunk[..read]);
2322                    if request.windows(4).any(|window| window == b"\r\n\r\n") {
2323                        break;
2324                    }
2325                    read = stream.read(&mut chunk).unwrap();
2326                }
2327                requests_for_thread
2328                    .lock()
2329                    .unwrap()
2330                    .push(String::from_utf8_lossy(&request).into_owned());
2331                stream.write_all(&response).unwrap();
2332            }
2333        });
2334        (port, requests, handle)
2335    }
2336
2337    #[test]
2338    fn builder_configures_client() {
2339        let proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
2340        let client = Client::builder()
2341            .redirect_policy(RedirectPolicy::follow(4))
2342            .connection_policy(ConnectionPolicy::Reuse)
2343            .cache_mode(CacheMode::Memory)
2344            .proxy(proxy)
2345            .build();
2346
2347        let session = client.session();
2348        let request = Request::get("http://example.com").unwrap();
2349        assert_eq!(session.config.redirect_policy, RedirectPolicy::follow(4));
2350        assert_eq!(session.config.parser_strictness, ParserStrictness::Strict);
2351        assert_eq!(request.method(), Method::Get);
2352    }
2353
2354    #[test]
2355    fn cache_control_parser_recognizes_directives() {
2356        let mut request = Request::get("http://example.com").unwrap();
2357        request
2358            .add_header(
2359                "Cache-Control",
2360                "no-store, max-age=30, no-cache, max-stale=10, min-fresh=4, only-if-cached",
2361            )
2362            .unwrap();
2363        let cache_control = CacheControl::from_headers(request.headers());
2364        assert!(cache_control.no_store);
2365        assert!(cache_control.no_cache);
2366        assert_eq!(cache_control.max_age, Some(30));
2367        assert_eq!(cache_control.max_stale, Some(Some(10)));
2368        assert_eq!(cache_control.min_fresh, Some(4));
2369        assert!(cache_control.only_if_cached);
2370    }
2371
2372    #[test]
2373    fn default_client_builder_matches_release_defaults() {
2374        let client = ClientBuilder::default().build();
2375        let session = client.session();
2376        assert_eq!(session.config.connection_policy, ConnectionPolicy::Close);
2377        assert_eq!(session.config.cache_mode, CacheMode::Disabled);
2378        assert_eq!(session.config.redirect_policy, RedirectPolicy::none());
2379    }
2380
2381    #[test]
2382    fn request_date_helpers_accept_cacheable_times() {
2383        let mut request = Request::get("http://example.com").unwrap();
2384        request
2385            .if_modified_since(UNIX_EPOCH + Duration::from_secs(784_111_777))
2386            .unwrap();
2387        assert!(request.header("if-modified-since").is_some());
2388    }
2389
2390    #[test]
2391    fn cache_control_parser_recognizes_bare_max_stale() {
2392        let mut request = Request::get("http://example.com").unwrap();
2393        request.add_header("Cache-Control", "max-stale").unwrap();
2394        let cache_control = CacheControl::from_headers(request.headers());
2395        assert_eq!(cache_control.max_stale, Some(None));
2396    }
2397
2398    #[test]
2399    fn cache_control_parser_recognizes_revalidation_and_visibility_directives() {
2400        let headers = vec![crate::request::Header::unchecked(
2401            "Cache-Control",
2402            "must-revalidate, proxy-revalidate, public, private",
2403        )];
2404        let cache_control = CacheControl::from_headers(&headers);
2405        assert!(cache_control.must_revalidate);
2406        assert!(cache_control.proxy_revalidate);
2407        assert!(cache_control.public);
2408        assert!(cache_control.private);
2409    }
2410
2411    #[test]
2412    fn cache_control_parser_is_case_insensitive_and_handles_quoted_values() {
2413        let headers = vec![crate::request::Header::unchecked(
2414            "Cache-Control",
2415            "MAX-AGE=\"60\", MIN-FRESH=5, ONLY-IF-CACHED",
2416        )];
2417        let cache_control = CacheControl::from_headers(&headers);
2418        assert_eq!(cache_control.max_age, Some(60));
2419        assert_eq!(cache_control.min_fresh, Some(5));
2420        assert!(cache_control.only_if_cached);
2421    }
2422
2423    #[test]
2424    fn builder_sets_parser_strictness() {
2425        let client = Client::builder()
2426            .parser_strictness(ParserStrictness::Lenient)
2427            .build();
2428        let session = client.session();
2429        assert_eq!(session.config.parser_strictness, ParserStrictness::Lenient);
2430    }
2431
2432    #[test]
2433    fn proxy_config_validates_scheme_and_headers() {
2434        let error = ProxyConfig::new("https://example.com").unwrap_err();
2435        assert!(matches!(
2436            error,
2437            crate::NanoGetError::UnsupportedProxyScheme(_)
2438        ));
2439
2440        let mut proxy = ProxyConfig::new("http://example.com:8080").unwrap();
2441        proxy.add_header("X-Proxy", "yes").unwrap();
2442        assert_eq!(proxy.url().authority_form(), "example.com:8080");
2443        assert_eq!(proxy.headers().len(), 1);
2444    }
2445
2446    #[test]
2447    fn validate_request_conditionals_checks_if_range_requirements() {
2448        let request = Request::get("http://example.com").unwrap();
2449        assert!(validate_request_conditionals(&request).is_ok());
2450
2451        let mut invalid = Request::get("http://example.com").unwrap();
2452        invalid.if_range("\"v1\"").unwrap();
2453        assert!(matches!(
2454            validate_request_conditionals(&invalid),
2455            Err(crate::NanoGetError::InvalidConditionalRequest(_))
2456        ));
2457
2458        let mut weak = Request::get("http://example.com").unwrap();
2459        weak.range_bytes(Some(0), Some(1)).unwrap();
2460        weak.if_range("W/\"v1\"").unwrap();
2461        assert!(matches!(
2462            validate_request_conditionals(&weak),
2463            Err(crate::NanoGetError::InvalidConditionalRequest(_))
2464        ));
2465    }
2466
2467    #[test]
2468    fn parse_and_range_helpers_cover_edge_cases() {
2469        assert!(parse_single_range("bytes=0-2").is_some());
2470        assert!(parse_single_range("bytes=2-").is_some());
2471        assert!(parse_single_range("bytes=-4").is_some());
2472        assert!(parse_single_range("bytes=0-1,3-4").is_none());
2473        assert!(parse_content_range("bytes 0-1/4").is_some());
2474        assert!(parse_content_range("invalid").is_none());
2475        assert!(is_strong_etag("\"v1\""));
2476        assert!(!is_strong_etag("W/\"v1\""));
2477
2478        let suffix = ByteRange {
2479            start: None,
2480            end: Some(2),
2481        };
2482        assert_eq!(suffix.resolve(5), Some((3, 4)));
2483        let invalid = ByteRange {
2484            start: Some(10),
2485            end: Some(12),
2486        };
2487        assert_eq!(invalid.resolve(5), None);
2488        let open_ended = ByteRange {
2489            start: Some(2),
2490            end: None,
2491        };
2492        assert_eq!(open_ended.resolve(5), Some((2, 4)));
2493        let empty = ByteRange {
2494            start: Some(0),
2495            end: Some(0),
2496        };
2497        assert_eq!(empty.resolve(0), None);
2498    }
2499
2500    #[test]
2501    fn if_range_matching_handles_etags_and_dates() {
2502        assert!(if_range_matches_entry(None, None, None));
2503        assert!(if_range_matches_entry(Some("\"v1\""), Some("\"v1\""), None));
2504        assert!(!if_range_matches_entry(
2505            Some("\"v1\""),
2506            Some("\"v2\""),
2507            None
2508        ));
2509        assert!(if_range_matches_entry(
2510            Some("Sun, 06 Nov 1994 08:49:37 GMT"),
2511            None,
2512            Some("Sun, 06 Nov 1994 08:49:37 GMT")
2513        ));
2514        assert!(!if_range_matches_entry(
2515            Some("Sun, 06 Nov 1994 08:49:37 GMT"),
2516            None,
2517            Some("Sun, 07 Nov 1994 08:49:37 GMT")
2518        ));
2519    }
2520
2521    #[test]
2522    fn normalize_segments_merges_adjacent_and_overlapping_ranges() {
2523        let mut segments = Vec::new();
2524        normalize_segments(&mut segments);
2525        assert!(segments.is_empty());
2526
2527        let mut segments = vec![
2528            ByteSegment {
2529                start: 0,
2530                end: 2,
2531                bytes: b"abc".to_vec(),
2532            },
2533            ByteSegment {
2534                start: 2,
2535                end: 4,
2536                bytes: b"cde".to_vec(),
2537            },
2538            ByteSegment {
2539                start: 8,
2540                end: 9,
2541                bytes: b"xy".to_vec(),
2542            },
2543        ];
2544        normalize_segments(&mut segments);
2545        assert_eq!(segments.len(), 2);
2546        assert_eq!(segments[0].bytes, b"abcde");
2547    }
2548
2549    #[test]
2550    fn cache_entry_range_response_handles_satisfiable_and_unsatisfiable_ranges() {
2551        let request = Request::get("http://example.com").unwrap();
2552        let cached = response(
2553            200,
2554            vec![crate::Header::unchecked("Content-Length", "6")],
2555            b"abcdef",
2556        );
2557        let entry =
2558            CacheEntry::new(&request, &timed_response(cached), AuthContext::default()).unwrap();
2559        let partial = entry.range_response(
2560            ByteRange {
2561                start: Some(1),
2562                end: Some(3),
2563            },
2564            SystemTime::now(),
2565        );
2566        assert_eq!(partial.status_code, 206);
2567        assert_eq!(partial.body, b"bcd");
2568
2569        let unsatisfiable = entry.range_response(
2570            ByteRange {
2571                start: Some(9),
2572                end: Some(12),
2573            },
2574            SystemTime::now(),
2575        );
2576        assert_eq!(unsatisfiable.status_code, 416);
2577    }
2578
2579    #[test]
2580    fn partial_cache_entry_promotes_complete_when_segments_cover_the_representation() {
2581        let request = Request::get("http://example.com").unwrap();
2582        let first = response(
2583            206,
2584            vec![
2585                crate::Header::unchecked("Cache-Control", "max-age=60"),
2586                crate::Header::unchecked("ETag", "\"v1\""),
2587                crate::Header::unchecked("Content-Range", "bytes 0-2/6"),
2588                crate::Header::unchecked("Content-Length", "3"),
2589            ],
2590            b"abc",
2591        );
2592        let second = response(
2593            206,
2594            vec![
2595                crate::Header::unchecked("Cache-Control", "max-age=60"),
2596                crate::Header::unchecked("ETag", "\"v1\""),
2597                crate::Header::unchecked("Content-Range", "bytes 3-5/6"),
2598                crate::Header::unchecked("Content-Length", "3"),
2599            ],
2600            b"def",
2601        );
2602        let mut partial =
2603            PartialCacheEntry::new(&request, &timed_response(first), AuthContext::default())
2604                .unwrap();
2605        partial.merge_from(
2606            PartialCacheEntry::new(&request, &timed_response(second), AuthContext::default())
2607                .unwrap(),
2608        );
2609        let promoted = partial.promote_complete().unwrap();
2610        assert_eq!(promoted.response.status_code, 200);
2611        assert_eq!(promoted.response.body, b"abcdef");
2612    }
2613
2614    #[test]
2615    fn memory_cache_stores_206_segments_and_promotes_to_full_entry() {
2616        let mut cache = MemoryCache::default();
2617        let url = "http://example.com/path";
2618
2619        let mut first = Request::get(url).unwrap();
2620        first.range_bytes(Some(0), Some(2)).unwrap();
2621        let response_one = response(
2622            206,
2623            vec![
2624                crate::Header::unchecked("Cache-Control", "max-age=60"),
2625                crate::Header::unchecked("ETag", "\"v1\""),
2626                crate::Header::unchecked("Content-Range", "bytes 0-2/6"),
2627                crate::Header::unchecked("Content-Length", "3"),
2628            ],
2629            b"abc",
2630        );
2631        cache.store(
2632            &first,
2633            &timed_response(response_one),
2634            &AuthContext::default(),
2635        );
2636
2637        let mut second = Request::get(url).unwrap();
2638        second.range_bytes(Some(3), Some(5)).unwrap();
2639        let response_two = response(
2640            206,
2641            vec![
2642                crate::Header::unchecked("Cache-Control", "max-age=60"),
2643                crate::Header::unchecked("ETag", "\"v1\""),
2644                crate::Header::unchecked("Content-Range", "bytes 3-5/6"),
2645                crate::Header::unchecked("Content-Length", "3"),
2646            ],
2647            b"def",
2648        );
2649        cache.store(
2650            &second,
2651            &timed_response(response_two),
2652            &AuthContext::default(),
2653        );
2654
2655        let full = Request::get(url).unwrap();
2656        let lookup = cache.lookup(&full, SystemTime::now(), &AuthContext::default());
2657        assert!(matches!(
2658            lookup,
2659            Some(super::CacheLookup::Fresh(ref response)) if response.body == b"abcdef"
2660        ));
2661    }
2662
2663    #[test]
2664    fn prepared_connect_headers_respects_request_and_preemptive_values() {
2665        let mut proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
2666        proxy.add_header("Authorization", "drop-me").unwrap();
2667        let mut request = Request::get("https://example.com").unwrap();
2668        request.proxy_authorization("Basic cmVxOnByb3h5").unwrap();
2669
2670        let config = ClientConfig {
2671            preemptive_proxy_authorization: Some("Basic cHJlZW1wdGl2ZQ==".to_string()),
2672            ..ClientConfig::default()
2673        };
2674
2675        let headers = prepared_connect_headers(&request, &config, &proxy).unwrap();
2676        assert!(headers
2677            .iter()
2678            .any(|header| header.matches_name("proxy-authorization")));
2679        assert!(!headers
2680            .iter()
2681            .any(|header| header.matches_name("authorization")));
2682
2683        let mut proxy_with_auth = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
2684        proxy_with_auth
2685            .add_header("Proxy-Authorization", "Basic cHJveHk=")
2686            .unwrap();
2687        let headers = prepared_connect_headers(&request, &config, &proxy_with_auth).unwrap();
2688        assert_eq!(
2689            headers
2690                .iter()
2691                .filter(|header| header.matches_name("proxy-authorization"))
2692                .count(),
2693            1
2694        );
2695        assert!(headers
2696            .iter()
2697            .any(|header| header.value() == "Basic cHJveHk="));
2698    }
2699
2700    #[test]
2701    fn maybe_retry_auth_handler_abort_maps_to_authentication_rejected() {
2702        let handler: Arc<dyn AuthHandler + Send + Sync> = Arc::new(AbortAuthHandler);
2703        let request = Request::get("http://example.com").unwrap();
2704        let response = response(
2705            401,
2706            vec![crate::Header::unchecked(
2707                "WWW-Authenticate",
2708                "Basic realm=\"api\"",
2709            )],
2710            b"",
2711        );
2712        let mut seen = Vec::new();
2713        let error = super::maybe_retry_request_auth(
2714            Some(&handler),
2715            AuthTarget::Origin,
2716            &request,
2717            &response,
2718            &mut seen,
2719        )
2720        .unwrap_err();
2721        assert!(matches!(
2722            error,
2723            crate::NanoGetError::AuthenticationRejected(_)
2724        ));
2725    }
2726
2727    #[test]
2728    fn execute_pipelined_validates_requests_before_network_io() {
2729        let mut close_session = Client::builder().build().session();
2730        let empty = close_session.execute_pipelined(&[]).unwrap();
2731        assert!(empty.is_empty());
2732
2733        let request = Request::get("http://example.com").unwrap();
2734        let error = close_session.execute_pipelined(&[request]).unwrap_err();
2735        assert!(matches!(error, crate::NanoGetError::Pipeline(_)));
2736
2737        let mut invalid_conditional = Request::get("http://example.com").unwrap();
2738        invalid_conditional.if_range("\"v1\"").unwrap();
2739        let mut reuse_session = Client::builder()
2740            .connection_policy(ConnectionPolicy::Reuse)
2741            .build()
2742            .session();
2743        let error = reuse_session
2744            .execute_pipelined(&[invalid_conditional])
2745            .unwrap_err();
2746        assert!(matches!(
2747            error,
2748            crate::NanoGetError::InvalidConditionalRequest(_)
2749        ));
2750
2751        let mut reuse_session = Client::builder()
2752            .connection_policy(ConnectionPolicy::Reuse)
2753            .build()
2754            .session();
2755        let error = reuse_session
2756            .execute_pipelined(&[
2757                Request::get("http://example.com/one").unwrap(),
2758                Request::get("http://example.org/two").unwrap(),
2759            ])
2760            .unwrap_err();
2761        assert!(matches!(error, crate::NanoGetError::Pipeline(_)));
2762
2763        let mut reuse_session = Client::builder()
2764            .connection_policy(ConnectionPolicy::Reuse)
2765            .build()
2766            .session();
2767        let error = reuse_session
2768            .execute_pipelined(&[
2769                Request::get("https://example.com/one").unwrap(),
2770                Request::get("https://example.org/two").unwrap(),
2771            ])
2772            .unwrap_err();
2773        assert!(matches!(error, crate::NanoGetError::Pipeline(_)));
2774    }
2775
2776    #[test]
2777    fn execute_follow_redirects_without_location_returns_original_response() {
2778        let (port, _requests, handle) = spawn_recording_server(vec![
2779            b"HTTP/1.1 302 Found\r\nContent-Length: 0\r\n\r\n".to_vec(),
2780        ]);
2781        let mut session = Client::builder().build().session();
2782        let request = Request::get(format!("http://127.0.0.1:{port}/"))
2783            .unwrap()
2784            .with_redirect_policy(RedirectPolicy::follow(3));
2785        let response = session.execute(request).unwrap();
2786        assert_eq!(response.status_code, 302);
2787        handle.join().unwrap();
2788    }
2789
2790    #[test]
2791    fn session_execute_ref_clones_and_executes_requests() {
2792        let (port, _requests, handle) = spawn_recording_server(vec![
2793            b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
2794        ]);
2795        let mut session = Client::builder().build().session();
2796        let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
2797        let response = session.execute_ref(&request).unwrap();
2798        assert_eq!(response.body, b"ok");
2799        handle.join().unwrap();
2800    }
2801
2802    #[test]
2803    fn execute_pipelined_handles_connection_close_on_final_response() {
2804        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2805        let port = listener.local_addr().unwrap().port();
2806        let handle = thread::spawn(move || {
2807            let (mut stream, _) = listener.accept().unwrap();
2808            let mut request = Vec::new();
2809            let mut chunk = [0u8; 256];
2810            loop {
2811                let read = stream.read(&mut chunk).unwrap();
2812                assert!(read > 0, "client closed before sending two requests");
2813                request.extend_from_slice(&chunk[..read]);
2814                if request
2815                    .windows(4)
2816                    .filter(|window| *window == b"\r\n\r\n")
2817                    .count()
2818                    >= 2
2819                {
2820                    break;
2821                }
2822            }
2823            if let Err(error) = stream.write_all(
2824                b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\naHTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 1\r\n\r\nb",
2825            ) {
2826                if error.kind() != std::io::ErrorKind::BrokenPipe {
2827                    panic!("unexpected server write failure: {error}");
2828                }
2829            }
2830        });
2831
2832        let mut session = Client::builder()
2833            .connection_policy(ConnectionPolicy::Reuse)
2834            .build()
2835            .session();
2836        let responses = session
2837            .execute_pipelined(&[
2838                Request::get(format!("http://127.0.0.1:{port}/one")).unwrap(),
2839                Request::get(format!("http://127.0.0.1:{port}/two")).unwrap(),
2840            ])
2841            .unwrap();
2842        assert_eq!(responses.len(), 2);
2843        assert_eq!(responses[0].body, b"a");
2844        assert_eq!(responses[1].body, b"b");
2845        assert!(session.connection.is_none());
2846        handle.join().unwrap();
2847    }
2848
2849    #[test]
2850    fn execute_one_only_if_cached_without_memory_cache_returns_gateway_timeout() {
2851        let mut session = Client::builder().build().session();
2852        let mut request = Request::get("http://example.com").unwrap();
2853        request
2854            .add_header("Cache-Control", "only-if-cached")
2855            .unwrap();
2856        let response = session.execute_one(request).unwrap();
2857        assert_eq!(response.status_code, 504);
2858    }
2859
2860    #[test]
2861    fn execute_one_uses_range_cache_hits_and_if_range_mismatch_fallback() {
2862        let (port, requests, handle) = spawn_recording_server(vec![
2863            b"HTTP/1.1 200 OK\r\nCache-Control: max-age=60\r\nContent-Length: 6\r\n\r\nghijkl"
2864                .to_vec(),
2865        ]);
2866        let url = format!("http://127.0.0.1:{port}/range");
2867        let request = Request::get(&url).unwrap();
2868        let cached = response(
2869            200,
2870            vec![
2871                Header::unchecked("Cache-Control", "max-age=60"),
2872                Header::unchecked("ETag", "\"v1\""),
2873                Header::unchecked("Content-Length", "6"),
2874            ],
2875            b"abcdef",
2876        );
2877        let mut session = Client::builder()
2878            .cache_mode(CacheMode::Memory)
2879            .build()
2880            .session();
2881        session.cache.lock().unwrap().store(
2882            &request,
2883            &timed_response(cached),
2884            &AuthContext::default(),
2885        );
2886
2887        let mut range_hit = Request::get(&url).unwrap();
2888        range_hit.range_bytes(Some(1), Some(3)).unwrap();
2889        let hit = session.execute_one(range_hit).unwrap();
2890        assert_eq!(hit.status_code, 206);
2891        assert_eq!(hit.body, b"bcd");
2892
2893        let mut mismatch = Request::get(&url).unwrap();
2894        mismatch.range_bytes(Some(0), Some(2)).unwrap();
2895        mismatch.if_range("\"different\"").unwrap();
2896        let network = session.execute_one(mismatch).unwrap();
2897        assert_eq!(network.status_code, 200);
2898        assert_eq!(network.body, b"ghijkl");
2899        assert_eq!(requests.lock().unwrap().len(), 1);
2900        handle.join().unwrap();
2901    }
2902
2903    #[test]
2904    fn execute_stale_adds_if_modified_since_when_last_modified_exists() {
2905        let (port, requests, handle) = spawn_recording_server(vec![
2906            b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
2907        ]);
2908        let request = Request::get(format!("http://127.0.0.1:{port}/stale")).unwrap();
2909        let stale_response = response(
2910            200,
2911            vec![
2912                Header::unchecked("Cache-Control", "max-age=0"),
2913                Header::unchecked("Last-Modified", "Sun, 06 Nov 1994 08:49:37 GMT"),
2914                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
2915                Header::unchecked("Content-Length", "4"),
2916            ],
2917            b"body",
2918        );
2919        let stale = CacheEntry::new(
2920            &request,
2921            &timed_response(stale_response),
2922            AuthContext::default(),
2923        )
2924        .unwrap();
2925        let mut session = Client::builder()
2926            .cache_mode(CacheMode::Memory)
2927            .build()
2928            .session();
2929        let response = session.execute_stale(request.clone(), stale).unwrap();
2930        assert_eq!(response.status_code, 200);
2931        let captured = requests.lock().unwrap().join("\n");
2932        assert!(captured.to_ascii_lowercase().contains("if-modified-since:"));
2933        handle.join().unwrap();
2934    }
2935
2936    #[test]
2937    fn execute_stale_keeps_existing_user_conditionals() {
2938        let (port, requests, handle) = spawn_recording_server(vec![
2939            b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
2940        ]);
2941        let mut request = Request::get(format!("http://127.0.0.1:{port}/stale")).unwrap();
2942        request.if_none_match("\"client\"").unwrap();
2943        let stale_response = response(
2944            200,
2945            vec![
2946                Header::unchecked("Cache-Control", "max-age=0"),
2947                Header::unchecked("ETag", "\"server\""),
2948                Header::unchecked("Last-Modified", "Sun, 06 Nov 1994 08:49:37 GMT"),
2949                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
2950                Header::unchecked("Content-Length", "4"),
2951            ],
2952            b"body",
2953        );
2954        let stale = CacheEntry::new(
2955            &request,
2956            &timed_response(stale_response),
2957            AuthContext::default(),
2958        )
2959        .unwrap();
2960        let mut session = Client::builder()
2961            .cache_mode(CacheMode::Memory)
2962            .build()
2963            .session();
2964        let response = session.execute_stale(request, stale).unwrap();
2965        assert_eq!(response.status_code, 200);
2966        let captured = requests.lock().unwrap().join("\n").to_ascii_lowercase();
2967        assert!(captured.contains("if-none-match: \"client\""));
2968        handle.join().unwrap();
2969    }
2970
2971    #[test]
2972    fn send_request_retries_network_errors_but_not_protocol_errors() {
2973        let mut session = Client::builder()
2974            .connection_policy(ConnectionPolicy::Reuse)
2975            .build()
2976            .session();
2977        let request = Request::get("http://127.0.0.1:9").unwrap();
2978        let error = session.send_request(&request).unwrap_err();
2979        assert!(matches!(
2980            error,
2981            crate::NanoGetError::Connect(_) | crate::NanoGetError::Io(_)
2982        ));
2983
2984        let (port, _requests, handle) = spawn_recording_server(vec![b"BROKEN\r\n\r\n".to_vec()]);
2985        let mut session = Client::builder()
2986            .connection_policy(ConnectionPolicy::Reuse)
2987            .build()
2988            .session();
2989        let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
2990        let error = session.send_request(&request).unwrap_err();
2991        assert!(matches!(error, crate::NanoGetError::MalformedStatusLine(_)));
2992        handle.join().unwrap();
2993    }
2994
2995    struct AlwaysIoStream;
2996
2997    impl Read for AlwaysIoStream {
2998        fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
2999            Err(std::io::Error::new(
3000                std::io::ErrorKind::Other,
3001                "forced read",
3002            ))
3003        }
3004    }
3005
3006    impl Write for AlwaysIoStream {
3007        fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
3008            Err(std::io::Error::new(
3009                std::io::ErrorKind::BrokenPipe,
3010                "forced write",
3011            ))
3012        }
3013
3014        fn flush(&mut self) -> std::io::Result<()> {
3015            Ok(())
3016        }
3017    }
3018
3019    #[test]
3020    fn send_request_retry_branch_runs_for_live_connection_io_errors() {
3021        let mut stream = AlwaysIoStream;
3022        let mut buf = [0u8; 1];
3023        assert!(stream.read(&mut buf).is_err());
3024        assert!(stream.write(&buf).is_err());
3025        stream.flush().unwrap();
3026
3027        let mut session = Client::builder()
3028            .connection_policy(ConnectionPolicy::Reuse)
3029            .build()
3030            .session();
3031        let request = Request::get("http://127.0.0.1:9").unwrap();
3032        session.connection = Some(super::LiveConnection {
3033            key: super::connection_key(&session.config.proxy, request.url()),
3034            reader: std::io::BufReader::new(Box::new(AlwaysIoStream)),
3035        });
3036        let error = session.send_request(&request).unwrap_err();
3037        assert!(matches!(
3038            error,
3039            crate::NanoGetError::Connect(_) | crate::NanoGetError::Io(_)
3040        ));
3041    }
3042
3043    #[test]
3044    fn execute_pipelined_propagates_response_parse_errors() {
3045        let (port, _requests, handle) = spawn_recording_server(vec![b"NOT-HTTP\r\n\r\n".to_vec()]);
3046        let mut session = Client::builder()
3047            .connection_policy(ConnectionPolicy::Reuse)
3048            .build()
3049            .session();
3050        let request = Request::get(format!("http://127.0.0.1:{port}/bad")).unwrap();
3051        let error = session.execute_pipelined(&[request]).unwrap_err();
3052        assert!(matches!(error, crate::NanoGetError::MalformedStatusLine(_)));
3053        handle.join().unwrap();
3054    }
3055
3056    #[test]
3057    fn pipeline_retry_classifier_only_retries_close_shaped_errors() {
3058        assert!(pipeline_retryable_parse_error(&crate::NanoGetError::Io(
3059            std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset"),
3060        )));
3061        assert!(pipeline_retryable_parse_error(
3062            &crate::NanoGetError::IncompleteMessage("unexpected EOF".to_string())
3063        ));
3064        assert!(!pipeline_retryable_parse_error(
3065            &crate::NanoGetError::MalformedStatusLine(
3066                "failed to read status line: unexpected EOF".to_string(),
3067            )
3068        ));
3069        assert!(!pipeline_retryable_parse_error(
3070            &crate::NanoGetError::MalformedHeader(
3071                "failed to read header line: unexpected EOF".to_string(),
3072            )
3073        ));
3074        assert!(!pipeline_retryable_parse_error(
3075            &crate::NanoGetError::MalformedStatusLine("HTTP/BROKEN".to_string())
3076        ));
3077        assert!(!pipeline_retryable_parse_error(
3078            &crate::NanoGetError::InvalidChunk("zz".to_string())
3079        ));
3080    }
3081
3082    #[test]
3083    fn reconnect_retry_classifier_includes_connect_and_tls_errors() {
3084        assert!(retryable_reconnect_error(&crate::NanoGetError::Connect(
3085            std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused"),
3086        )));
3087        assert!(retryable_reconnect_error(&crate::NanoGetError::Tls(
3088            "handshake failed".to_string(),
3089        )));
3090        assert!(!retryable_reconnect_error(
3091            &crate::NanoGetError::MalformedStatusLine(
3092                "failed to read status line: unexpected EOF".to_string(),
3093            )
3094        ));
3095        assert!(!retryable_reconnect_error(
3096            &crate::NanoGetError::MalformedStatusLine("BROKEN".to_string())
3097        ));
3098    }
3099
3100    #[test]
3101    fn send_ephemeral_propagates_prepared_request_failures() {
3102        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
3103        let port = listener.local_addr().unwrap().port();
3104        let handle = thread::spawn(move || {
3105            let _ = listener.accept().unwrap();
3106        });
3107
3108        let proxy = super::ProxyConfig {
3109            url: Url::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(),
3110            headers: vec![Header::unchecked("X-Bad", "line\nbreak")],
3111        };
3112        let session = Client::builder().proxy(proxy).build().session();
3113        let request = Request::get("http://example.com").unwrap();
3114        let error = session.send_ephemeral(&request).unwrap_err();
3115        assert!(matches!(error, crate::NanoGetError::InvalidHeaderValue(_)));
3116        handle.join().unwrap();
3117    }
3118
3119    #[test]
3120    fn send_helpers_cover_ephemeral_and_live_connection_paths() {
3121        let (port, _requests, handle) = spawn_recording_server(vec![
3122            b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".to_vec(),
3123        ]);
3124        let session = Client::builder().build().session();
3125        let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
3126        let timed = session.send_ephemeral(&request).unwrap();
3127        assert_eq!(timed.response.body, b"ok");
3128        handle.join().unwrap();
3129
3130        let mut session = Client::builder()
3131            .connection_policy(ConnectionPolicy::Reuse)
3132            .build()
3133            .session();
3134        let error = session.send_on_live_connection(&request).unwrap_err();
3135        assert!(matches!(error, crate::NanoGetError::Io(_)));
3136
3137        let (port, _requests, handle) = spawn_recording_server(vec![
3138            b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nok".to_vec(),
3139        ]);
3140        let request = Request::get(format!("http://127.0.0.1:{port}/")).unwrap();
3141        session.ensure_connection(&request).unwrap();
3142        let timed = session.send_on_live_connection(&request).unwrap();
3143        assert_eq!(timed.response.status_code, 200);
3144        assert!(session.connection.is_none());
3145        handle.join().unwrap();
3146    }
3147
3148    #[test]
3149    fn range_lookup_covers_stale_and_partial_only_if_cached_paths() {
3150        let mut cache = MemoryCache::default();
3151        let request = Request::get("http://example.com/stale-range").unwrap();
3152        let stale_response = response(
3153            200,
3154            vec![
3155                Header::unchecked("Cache-Control", "max-age=0"),
3156                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3157                Header::unchecked("ETag", "\"stale\""),
3158                Header::unchecked("Content-Length", "6"),
3159            ],
3160            b"abcdef",
3161        );
3162        cache.store(
3163            &request,
3164            &timed_response(stale_response),
3165            &AuthContext::default(),
3166        );
3167        let mut stale_range = Request::get("http://example.com/stale-range").unwrap();
3168        stale_range.range_bytes(Some(0), Some(2)).unwrap();
3169        assert!(cache
3170            .lookup_range(&stale_range, SystemTime::now(), &AuthContext::default())
3171            .is_none());
3172
3173        let mut partial_request = Request::get("http://example.com/partial-only").unwrap();
3174        partial_request.range_bytes(Some(0), Some(2)).unwrap();
3175        let partial_response = response(
3176            206,
3177            vec![
3178                Header::unchecked("Cache-Control", "max-age=60"),
3179                Header::unchecked("ETag", "\"p2\""),
3180                Header::unchecked("Content-Range", "bytes 0-2/6"),
3181                Header::unchecked("Content-Length", "3"),
3182            ],
3183            b"abc",
3184        );
3185        cache.store(
3186            &partial_request,
3187            &timed_response(partial_response),
3188            &AuthContext::default(),
3189        );
3190
3191        let mut mismatch = Request::get("http://example.com/partial-only").unwrap();
3192        mismatch.range_bytes(Some(0), Some(1)).unwrap();
3193        mismatch.if_range("\"different\"").unwrap();
3194        mismatch
3195            .add_header("Cache-Control", "only-if-cached")
3196            .unwrap();
3197        assert!(matches!(
3198            cache.lookup_range(&mismatch, SystemTime::now(), &AuthContext::default()),
3199            Some(super::RangeCacheLookup::UnsatisfiedOnlyIfCached)
3200        ));
3201
3202        let mut no_segment = Request::get("http://example.com/partial-only").unwrap();
3203        no_segment.range_bytes(Some(4), Some(5)).unwrap();
3204        assert!(cache
3205            .lookup_range(&no_segment, SystemTime::now(), &AuthContext::default())
3206            .is_none());
3207    }
3208
3209    #[test]
3210    fn prepared_request_applies_proxy_headers_for_http_proxy_targets() {
3211        let mut proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
3212        proxy.add_header("X-Proxy", "yes").unwrap();
3213        let config = ClientConfig {
3214            proxy: Some(proxy),
3215            ..ClientConfig::default()
3216        };
3217        let request = Request::get("http://example.com").unwrap();
3218        let prepared =
3219            super::prepared_request(&request, &config, super::SendTarget::HttpProxy).unwrap();
3220        assert_eq!(prepared.header("x-proxy"), Some("yes"));
3221
3222        let https_request = Request::get("https://example.com").unwrap();
3223        let tunnel =
3224            super::prepared_request(&https_request, &config, super::SendTarget::Tunnel).unwrap();
3225        assert_eq!(tunnel.header("x-proxy"), None);
3226    }
3227
3228    #[test]
3229    fn open_connection_rejects_unknown_url_schemes() {
3230        let mut url = Url::parse("http://example.com").unwrap();
3231        url.scheme = "ws".to_string();
3232        let request = Request::new(Method::Get, url).unwrap();
3233        let error = super::open_connection(&ClientConfig::default(), &request)
3234            .err()
3235            .expect("unexpected successful connection");
3236        assert!(matches!(error, crate::NanoGetError::UnsupportedScheme(_)));
3237    }
3238
3239    #[test]
3240    fn proxy_send_target_and_connection_keys_cover_tunnel_variants() {
3241        let proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
3242        let config = ClientConfig {
3243            proxy: Some(proxy),
3244            ..ClientConfig::default()
3245        };
3246
3247        let http_request = Request::get("http://example.com").unwrap();
3248        assert!(matches!(
3249            super::SendTarget::for_request(&config, &http_request),
3250            super::SendTarget::HttpProxy
3251        ));
3252
3253        let https_request = Request::get("https://example.com").unwrap();
3254        assert!(matches!(
3255            super::SendTarget::for_request(&config, &https_request),
3256            super::SendTarget::Tunnel
3257        ));
3258
3259        let key = super::connection_key(&config.proxy, https_request.url());
3260        assert!(matches!(key, super::ConnectionKey::HttpsTunnel { .. }));
3261    }
3262
3263    #[test]
3264    fn open_connection_with_proxy_https_path_reaches_tunnel_branch() {
3265        let proxy = ProxyConfig::new("http://127.0.0.1:9").unwrap();
3266        let config = ClientConfig {
3267            proxy: Some(proxy),
3268            ..ClientConfig::default()
3269        };
3270        let request = Request::get("https://example.com").unwrap();
3271        let error = super::open_connection(&config, &request)
3272            .err()
3273            .expect("unexpected successful connection");
3274        assert!(matches!(
3275            error,
3276            crate::NanoGetError::Connect(_)
3277                | crate::NanoGetError::Io(_)
3278                | crate::NanoGetError::HttpsFeatureRequired
3279                | crate::NanoGetError::Tls(_)
3280                | crate::NanoGetError::ProxyConnectFailed(_, _)
3281        ));
3282    }
3283
3284    #[test]
3285    fn memory_cache_lookup_and_range_paths_cover_only_if_cached_and_if_range_logic() {
3286        let mut cache = MemoryCache::default();
3287        let mut only_if_cached = Request::get("http://example.com").unwrap();
3288        only_if_cached
3289            .add_header("Cache-Control", "only-if-cached")
3290            .unwrap();
3291        assert!(matches!(
3292            cache.lookup(&only_if_cached, SystemTime::now(), &AuthContext::default()),
3293            Some(super::CacheLookup::UnsatisfiedOnlyIfCached)
3294        ));
3295
3296        let mut seeded_request = Request::get("http://example.com").unwrap();
3297        seeded_request.add_header("Accept", "text/plain").unwrap();
3298        let seeded_response = response(
3299            200,
3300            vec![
3301                Header::unchecked("Cache-Control", "max-age=60"),
3302                Header::unchecked("Vary", "Accept"),
3303                Header::unchecked("ETag", "\"v1\""),
3304            ],
3305            b"abcdef",
3306        );
3307        cache.store(
3308            &seeded_request,
3309            &timed_response(seeded_response),
3310            &AuthContext::default(),
3311        );
3312
3313        let mut mismatched = Request::get("http://example.com").unwrap();
3314        mismatched.add_header("Accept", "application/json").unwrap();
3315        mismatched
3316            .add_header("Cache-Control", "only-if-cached")
3317            .unwrap();
3318        assert!(matches!(
3319            cache.lookup(&mismatched, SystemTime::now(), &AuthContext::default()),
3320            Some(super::CacheLookup::UnsatisfiedOnlyIfCached)
3321        ));
3322
3323        let mut range_hit = Request::get("http://example.com").unwrap();
3324        range_hit.add_header("Accept", "text/plain").unwrap();
3325        range_hit.range_bytes(Some(1), Some(3)).unwrap();
3326        let lookup = cache.lookup_range(&range_hit, SystemTime::now(), &AuthContext::default());
3327        assert!(matches!(lookup, Some(super::RangeCacheLookup::Hit(_))));
3328
3329        let mut range_mismatch = Request::get("http://example.com").unwrap();
3330        range_mismatch.add_header("Accept", "text/plain").unwrap();
3331        range_mismatch.range_bytes(Some(0), Some(1)).unwrap();
3332        range_mismatch.if_range("\"v2\"").unwrap();
3333        assert!(matches!(
3334            cache.lookup_range(&range_mismatch, SystemTime::now(), &AuthContext::default()),
3335            Some(super::RangeCacheLookup::IfRangeMismatch)
3336        ));
3337
3338        let mut range_mismatch_only_cached = Request::get("http://example.com").unwrap();
3339        range_mismatch_only_cached
3340            .add_header("Accept", "text/plain")
3341            .unwrap();
3342        range_mismatch_only_cached
3343            .range_bytes(Some(0), Some(1))
3344            .unwrap();
3345        range_mismatch_only_cached.if_range("\"v2\"").unwrap();
3346        range_mismatch_only_cached
3347            .add_header("Cache-Control", "only-if-cached")
3348            .unwrap();
3349        assert!(matches!(
3350            cache.lookup_range(
3351                &range_mismatch_only_cached,
3352                SystemTime::now(),
3353                &AuthContext::default()
3354            ),
3355            Some(super::RangeCacheLookup::UnsatisfiedOnlyIfCached)
3356        ));
3357
3358        let mut only_if_cached_range = Request::get("http://example.net").unwrap();
3359        only_if_cached_range.range_bytes(Some(0), Some(1)).unwrap();
3360        only_if_cached_range
3361            .add_header("Cache-Control", "only-if-cached")
3362            .unwrap();
3363        assert!(matches!(
3364            cache.lookup_range(
3365                &only_if_cached_range,
3366                SystemTime::now(),
3367                &AuthContext::default()
3368            ),
3369            Some(super::RangeCacheLookup::UnsatisfiedOnlyIfCached)
3370        ));
3371    }
3372
3373    #[test]
3374    fn partial_cache_lookup_and_promotion_cover_additional_branches() {
3375        let mut cache = MemoryCache::default();
3376        let mut partial_request = Request::get("http://example.com/partial").unwrap();
3377        partial_request.range_bytes(Some(0), Some(2)).unwrap();
3378        let partial_response = response(
3379            206,
3380            vec![
3381                Header::unchecked("Cache-Control", "max-age=60"),
3382                Header::unchecked("ETag", "\"part\""),
3383                Header::unchecked("Content-Range", "bytes 0-2/6"),
3384                Header::unchecked("Content-Length", "3"),
3385            ],
3386            b"abc",
3387        );
3388        cache.store(
3389            &partial_request,
3390            &timed_response(partial_response),
3391            &AuthContext::default(),
3392        );
3393
3394        let mut hit_request = Request::get("http://example.com/partial").unwrap();
3395        hit_request.range_bytes(Some(1), Some(2)).unwrap();
3396        let lookup = cache.lookup_range(&hit_request, SystemTime::now(), &AuthContext::default());
3397        assert!(matches!(lookup, Some(super::RangeCacheLookup::Hit(_))));
3398
3399        let mut miss_request = Request::get("http://example.com/partial").unwrap();
3400        miss_request.range_bytes(Some(4), Some(5)).unwrap();
3401        assert!(cache
3402            .lookup_range(&miss_request, SystemTime::now(), &AuthContext::default())
3403            .is_none());
3404
3405        let mut mismatch = Request::get("http://example.com/partial").unwrap();
3406        mismatch.range_bytes(Some(0), Some(1)).unwrap();
3407        mismatch.if_range("\"different\"").unwrap();
3408        assert!(matches!(
3409            cache.lookup_range(&mismatch, SystemTime::now(), &AuthContext::default()),
3410            Some(super::RangeCacheLookup::IfRangeMismatch)
3411        ));
3412
3413        let invalid_partial = response(
3414            206,
3415            vec![
3416                Header::unchecked("Cache-Control", "max-age=60"),
3417                Header::unchecked("ETag", "\"part\""),
3418            ],
3419            b"abc",
3420        );
3421        cache.store(
3422            &partial_request,
3423            &timed_response(invalid_partial),
3424            &AuthContext::default(),
3425        );
3426    }
3427
3428    #[test]
3429    fn cache_entry_and_partial_entry_helpers_cover_unhit_branches() {
3430        let request = Request::get("http://example.com/cache").unwrap();
3431        let base_response = response(
3432            200,
3433            vec![
3434                Header::unchecked("Cache-Control", "max-age=2"),
3435                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3436                Header::unchecked("ETag", "\"v1\""),
3437                Header::unchecked("Content-Length", "6"),
3438            ],
3439            b"abcdef",
3440        );
3441        let mut entry = CacheEntry::new(
3442            &request,
3443            &timed_response(base_response),
3444            AuthContext::default(),
3445        )
3446        .unwrap();
3447        entry.cache_control.no_cache = true;
3448        assert!(!entry.is_fresh(SystemTime::now()));
3449        entry.cache_control.no_cache = false;
3450        entry.date_header = Some(entry.response_time + Duration::from_secs(1));
3451        let now = entry.response_time + Duration::from_secs(3);
3452        let _ = entry.current_age(now);
3453        let _ = entry.remaining_freshness(now);
3454        let _ = entry.staleness(now);
3455        assert!(!entry.satisfies_request(
3456            &CacheControl {
3457                max_age: Some(1),
3458                ..CacheControl::default()
3459            },
3460            now
3461        ));
3462        assert!(!entry.satisfies_request(
3463            &CacheControl {
3464                min_fresh: Some(99),
3465                ..CacheControl::default()
3466            },
3467            now
3468        ));
3469        entry.freshness_lifetime = Duration::from_secs(0);
3470        assert!(entry.satisfies_request(
3471            &CacheControl {
3472                max_stale: Some(Some(10)),
3473                ..CacheControl::default()
3474            },
3475            now
3476        ));
3477
3478        let partial_response = response(
3479            206,
3480            vec![
3481                Header::unchecked("Cache-Control", "max-age=1"),
3482                Header::unchecked("ETag", "\"p1\""),
3483                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3484                Header::unchecked("Content-Range", "bytes 0-2/3"),
3485                Header::unchecked("Content-Length", "3"),
3486            ],
3487            b"abc",
3488        );
3489        let mut partial = PartialCacheEntry::new(
3490            &request,
3491            &timed_response(partial_response),
3492            AuthContext::default(),
3493        )
3494        .unwrap();
3495        partial.date_header = Some(partial.response_time + Duration::from_secs(1));
3496        let now = partial.response_time + Duration::from_secs(2);
3497        let _ = partial.current_age(now);
3498        let _ = partial.remaining_freshness(now);
3499        let _ = partial.staleness(now);
3500        partial.cache_control.no_cache = true;
3501        assert!(!partial.is_fresh(now));
3502        assert!(!partial.satisfies_request(&CacheControl::default(), now));
3503        partial.cache_control.no_cache = false;
3504        assert!(!partial.satisfies_request(
3505            &CacheControl {
3506                max_age: Some(0),
3507                ..CacheControl::default()
3508            },
3509            now
3510        ));
3511        assert!(!partial.satisfies_request(
3512            &CacheControl {
3513                min_fresh: Some(100),
3514                ..CacheControl::default()
3515            },
3516            now
3517        ));
3518        partial.freshness_lifetime = Duration::from_secs(0);
3519        assert!(partial.satisfies_request(
3520            &CacheControl {
3521                max_stale: Some(Some(10)),
3522                ..CacheControl::default()
3523            },
3524            now
3525        ));
3526        let range = partial
3527            .range_response(
3528                ByteRange {
3529                    start: Some(1),
3530                    end: Some(2),
3531                },
3532                now,
3533            )
3534            .unwrap();
3535        assert_eq!(range.status_code, 206);
3536        assert_eq!(range.body, b"bc");
3537
3538        partial.segments.push(ByteSegment {
3539            start: 10,
3540            end: 11,
3541            bytes: b"zz".to_vec(),
3542        });
3543        assert!(partial.promote_complete().is_none());
3544    }
3545
3546    #[test]
3547    fn cache_satisfies_request_specific_branches_are_exercised() {
3548        let request = Request::get("http://example.com/age").unwrap();
3549        let base = response(
3550            200,
3551            vec![
3552                Header::unchecked("Cache-Control", "max-age=100"),
3553                Header::unchecked("Content-Length", "1"),
3554            ],
3555            b"x",
3556        );
3557        let mut entry =
3558            CacheEntry::new(&request, &timed_response(base), AuthContext::default()).unwrap();
3559        entry.request_time = UNIX_EPOCH;
3560        entry.response_time = UNIX_EPOCH + Duration::from_secs(10);
3561        entry.date_header = Some(UNIX_EPOCH);
3562        let now = UNIX_EPOCH + Duration::from_secs(20);
3563        assert!(!entry.satisfies_request(
3564            &CacheControl {
3565                max_age: Some(1),
3566                ..CacheControl::default()
3567            },
3568            now
3569        ));
3570        assert!(!entry.satisfies_request(
3571            &CacheControl {
3572                min_fresh: Some(200),
3573                ..CacheControl::default()
3574            },
3575            now
3576        ));
3577
3578        let partial_response = response(
3579            206,
3580            vec![
3581                Header::unchecked("Cache-Control", "max-age=100"),
3582                Header::unchecked("ETag", "\"p-age\""),
3583                Header::unchecked("Content-Range", "bytes 0-0/1"),
3584                Header::unchecked("Content-Length", "1"),
3585            ],
3586            b"x",
3587        );
3588        let mut partial = PartialCacheEntry::new(
3589            &request,
3590            &timed_response(partial_response),
3591            AuthContext::default(),
3592        )
3593        .unwrap();
3594        partial.request_time = UNIX_EPOCH;
3595        partial.response_time = UNIX_EPOCH + Duration::from_secs(10);
3596        partial.date_header = Some(UNIX_EPOCH);
3597        let now = UNIX_EPOCH + Duration::from_secs(20);
3598        assert!(!partial.satisfies_request(
3599            &CacheControl {
3600                max_age: Some(1),
3601                ..CacheControl::default()
3602            },
3603            now
3604        ));
3605        assert!(!partial.satisfies_request(
3606            &CacheControl {
3607                min_fresh: Some(200),
3608                ..CacheControl::default()
3609            },
3610            now
3611        ));
3612        partial.freshness_lifetime = Duration::from_secs(0);
3613        partial.cache_control.must_revalidate = true;
3614        assert!(!partial.satisfies_request(
3615            &CacheControl {
3616                max_stale: Some(None),
3617                ..CacheControl::default()
3618            },
3619            now
3620        ));
3621        partial.cache_control.must_revalidate = false;
3622        assert!(partial.satisfies_request(
3623            &CacheControl {
3624                max_stale: Some(None),
3625                ..CacheControl::default()
3626            },
3627            now
3628        ));
3629        assert!(!partial.satisfies_request(&CacheControl::default(), now));
3630    }
3631
3632    #[test]
3633    fn cache_construction_rejections_and_misc_helpers_cover_branches() {
3634        let request = Request::get("http://example.com/path").unwrap();
3635        let now = SystemTime::now();
3636
3637        let no_store = response(
3638            200,
3639            vec![Header::unchecked("Cache-Control", "no-store")],
3640            b"x",
3641        );
3642        assert!(
3643            CacheEntry::new(&request, &timed_response(no_store), AuthContext::default()).is_none()
3644        );
3645
3646        let cacheable = response(
3647            200,
3648            vec![Header::unchecked("Cache-Control", "max-age=60")],
3649            b"x",
3650        );
3651        assert!(CacheEntry::new(
3652            &request,
3653            &timed_response(cacheable.clone()),
3654            AuthContext {
3655                origin: None,
3656                proxy: Some("p".to_string())
3657            }
3658        )
3659        .is_none());
3660
3661        let no_store_partial = response(
3662            206,
3663            vec![
3664                Header::unchecked("Cache-Control", "no-store"),
3665                Header::unchecked("ETag", "\"v1\""),
3666                Header::unchecked("Content-Range", "bytes 0-0/1"),
3667                Header::unchecked("Content-Length", "1"),
3668            ],
3669            b"x",
3670        );
3671        assert!(PartialCacheEntry::new(
3672            &request,
3673            &timed_response(no_store_partial),
3674            AuthContext::default()
3675        )
3676        .is_none());
3677
3678        let proxy_partial = response(
3679            206,
3680            vec![
3681                Header::unchecked("Cache-Control", "max-age=60"),
3682                Header::unchecked("ETag", "\"v1\""),
3683                Header::unchecked("Content-Range", "bytes 0-0/1"),
3684                Header::unchecked("Content-Length", "1"),
3685            ],
3686            b"x",
3687        );
3688        assert!(PartialCacheEntry::new(
3689            &request,
3690            &timed_response(proxy_partial.clone()),
3691            AuthContext {
3692                origin: None,
3693                proxy: Some("p".to_string())
3694            }
3695        )
3696        .is_none());
3697        assert!(PartialCacheEntry::new(
3698            &request,
3699            &timed_response(proxy_partial.clone()),
3700            AuthContext {
3701                origin: Some("secret".to_string()),
3702                proxy: None
3703            }
3704        )
3705        .is_none());
3706
3707        let invalid_range = response(
3708            206,
3709            vec![
3710                Header::unchecked("Cache-Control", "max-age=60"),
3711                Header::unchecked("ETag", "\"v1\""),
3712                Header::unchecked("Content-Range", "bytes 0-4/10"),
3713                Header::unchecked("Content-Length", "5"),
3714            ],
3715            b"abc",
3716        );
3717        assert!(PartialCacheEntry::new(
3718            &request,
3719            &timed_response(invalid_range),
3720            AuthContext::default()
3721        )
3722        .is_none());
3723
3724        let weak_etag = response(
3725            206,
3726            vec![
3727                Header::unchecked("Cache-Control", "max-age=60"),
3728                Header::unchecked("ETag", "W/\"v1\""),
3729                Header::unchecked("Content-Range", "bytes 0-2/3"),
3730                Header::unchecked("Content-Length", "3"),
3731            ],
3732            b"abc",
3733        );
3734        assert!(PartialCacheEntry::new(
3735            &request,
3736            &timed_response(weak_etag),
3737            AuthContext::default()
3738        )
3739        .is_none());
3740
3741        let mut cache = MemoryCache::default();
3742        let mut get = Request::get("http://example.com/upsert").unwrap();
3743        get.add_header("Accept", "text/plain").unwrap();
3744        let first = response(
3745            200,
3746            vec![
3747                Header::unchecked("Cache-Control", "max-age=60"),
3748                Header::unchecked("Vary", "Accept"),
3749                Header::unchecked("Content-Length", "1"),
3750            ],
3751            b"a",
3752        );
3753        let second = response(
3754            200,
3755            vec![
3756                Header::unchecked("Cache-Control", "max-age=60"),
3757                Header::unchecked("Vary", "Accept"),
3758                Header::unchecked("Content-Length", "1"),
3759            ],
3760            b"b",
3761        );
3762        let first_entry =
3763            CacheEntry::new(&get, &timed_response(first), AuthContext::default()).unwrap();
3764        let second_entry =
3765            CacheEntry::new(&get, &timed_response(second), AuthContext::default()).unwrap();
3766        cache.upsert_complete_entry(get.url().cache_key(), first_entry);
3767        cache.upsert_complete_entry(get.url().cache_key(), second_entry);
3768        assert_eq!(cache.entries[&get.url().cache_key()].len(), 1);
3769
3770        let direct = CacheControl::from_headers(&[Header::unchecked("Cache-Control", "x-test=1")]);
3771        assert!(!direct.no_store);
3772        let head_response = super::response_for_method(
3773            cache.entries[&get.url().cache_key()][0].response.clone(),
3774            Method::Head,
3775        );
3776        assert!(head_response.body.is_empty());
3777
3778        let vary_star_response = response(200, vec![Header::unchecked("Vary", "*")], b"");
3779        assert!(super::extract_vary_headers(&request, &vary_star_response).is_none());
3780
3781        assert!(super::is_cacheable_status(203));
3782        assert!(!super::is_cacheable_status(418));
3783
3784        let expires = response(
3785            200,
3786            vec![
3787                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3788                Header::unchecked("Expires", "Sun, 06 Nov 1994 08:50:37 GMT"),
3789            ],
3790            b"",
3791        );
3792        assert_eq!(
3793            super::compute_freshness_lifetime(&expires, now),
3794            Duration::from_secs(60)
3795        );
3796
3797        let heuristic = response(
3798            200,
3799            vec![
3800                Header::unchecked("Date", "Sun, 06 Nov 1994 08:49:37 GMT"),
3801                Header::unchecked("Last-Modified", "Sun, 06 Nov 1994 08:39:37 GMT"),
3802            ],
3803            b"",
3804        );
3805        assert_eq!(
3806            super::compute_freshness_lifetime(&heuristic, now),
3807            Duration::from_secs(60)
3808        );
3809
3810        let expires_without_date = response(
3811            200,
3812            vec![Header::unchecked(
3813                "Expires",
3814                "Sun, 06 Nov 1994 08:50:37 GMT",
3815            )],
3816            b"",
3817        );
3818        assert_eq!(
3819            super::compute_freshness_lifetime(
3820                &expires_without_date,
3821                UNIX_EPOCH + Duration::from_secs(784_111_700)
3822            ),
3823            Duration::from_secs(137)
3824        );
3825
3826        let existing = CacheEntry::new(
3827            &request,
3828            &timed_response(response(
3829                200,
3830                vec![
3831                    Header::unchecked("Cache-Control", "max-age=60"),
3832                    Header::unchecked("ETag", "\"v1\""),
3833                    Header::unchecked("Content-Length", "3"),
3834                ],
3835                b"abc",
3836            )),
3837            AuthContext::default(),
3838        )
3839        .unwrap();
3840        let different_etag = CacheEntry::new(
3841            &request,
3842            &timed_response(response(
3843                200,
3844                vec![
3845                    Header::unchecked("Cache-Control", "max-age=60"),
3846                    Header::unchecked("ETag", "\"v2\""),
3847                    Header::unchecked("Content-Length", "3"),
3848                ],
3849                b"abc",
3850            )),
3851            AuthContext::default(),
3852        )
3853        .unwrap();
3854        assert!(!super::head_update_is_compatible(
3855            &existing,
3856            &different_etag
3857        ));
3858        let different_length = CacheEntry::new(
3859            &request,
3860            &timed_response(response(
3861                200,
3862                vec![
3863                    Header::unchecked("Cache-Control", "max-age=60"),
3864                    Header::unchecked("ETag", "\"v1\""),
3865                    Header::unchecked("Content-Length", "4"),
3866                ],
3867                b"abcd",
3868            )),
3869            AuthContext::default(),
3870        )
3871        .unwrap();
3872        assert!(!super::head_update_is_compatible(
3873            &existing,
3874            &different_length
3875        ));
3876        let same_length = CacheEntry::new(
3877            &request,
3878            &timed_response(response(
3879                200,
3880                vec![
3881                    Header::unchecked("Cache-Control", "max-age=60"),
3882                    Header::unchecked("ETag", "\"v1\""),
3883                    Header::unchecked("Content-Length", "3"),
3884                ],
3885                b"abc",
3886            )),
3887            AuthContext::default(),
3888        )
3889        .unwrap();
3890        assert!(super::head_update_is_compatible(&existing, &same_length));
3891
3892        assert!(!super::if_range_matches_entry(
3893            Some("invalid-date"),
3894            None,
3895            Some("Sun, 06 Nov 1994 08:49:37 GMT")
3896        ));
3897        assert!(!super::if_range_matches_entry(
3898            Some("Sun, 06 Nov 1994 08:49:37 GMT"),
3899            None,
3900            None
3901        ));
3902    }
3903
3904    #[test]
3905    fn authentication_retry_helper_covers_remaining_branches() {
3906        let request = Request::get("http://example.com").unwrap();
3907        let mut seen = Vec::new();
3908        let empty_challenge_response =
3909            response(401, vec![Header::unchecked("WWW-Authenticate", "")], b"");
3910        assert!(super::maybe_retry_request_auth(
3911            None,
3912            AuthTarget::Origin,
3913            &request,
3914            &empty_challenge_response,
3915            &mut seen
3916        )
3917        .unwrap()
3918        .is_none());
3919
3920        let challenge_response = response(
3921            401,
3922            vec![Header::unchecked("WWW-Authenticate", "Basic realm=\"api\"")],
3923            b"",
3924        );
3925        assert!(super::maybe_retry_request_auth(
3926            None,
3927            AuthTarget::Origin,
3928            &request,
3929            &challenge_response,
3930            &mut seen
3931        )
3932        .unwrap()
3933        .is_none());
3934        assert!(super::maybe_retry_request_auth(
3935            None,
3936            AuthTarget::Origin,
3937            &request,
3938            &challenge_response,
3939            &mut seen
3940        )
3941        .unwrap()
3942        .is_none());
3943
3944        let no_match_handler: Arc<dyn AuthHandler + Send + Sync> = Arc::new(NoMatchAuthHandler);
3945        assert!(super::maybe_retry_request_auth(
3946            Some(&no_match_handler),
3947            AuthTarget::Origin,
3948            &request,
3949            &challenge_response,
3950            &mut Vec::new()
3951        )
3952        .unwrap()
3953        .is_none());
3954
3955        let use_headers_handler: Arc<dyn AuthHandler + Send + Sync> =
3956            Arc::new(UseHeadersAuthHandler {
3957                header_name: "Authorization",
3958                header_value: "Basic dXNlcjpwYXNz",
3959            });
3960        let retried = super::maybe_retry_request_auth(
3961            Some(&use_headers_handler),
3962            AuthTarget::Origin,
3963            &request,
3964            &challenge_response,
3965            &mut Vec::new(),
3966        )
3967        .unwrap()
3968        .unwrap();
3969        assert_eq!(retried.header("authorization"), Some("Basic dXNlcjpwYXNz"));
3970
3971        let abort_handler: Arc<dyn AuthHandler + Send + Sync> = Arc::new(AbortAuthHandler);
3972        let proxy_response = response(
3973            407,
3974            vec![Header::unchecked(
3975                "Proxy-Authenticate",
3976                "Basic realm=\"proxy\"",
3977            )],
3978            b"",
3979        );
3980        let error = super::maybe_retry_request_auth(
3981            Some(&abort_handler),
3982            AuthTarget::Proxy,
3983            &request,
3984            &proxy_response,
3985            &mut Vec::new(),
3986        )
3987        .unwrap_err();
3988        assert!(matches!(
3989            error,
3990            crate::NanoGetError::AuthenticationRejected(message)
3991            if message.contains("proxy authentication handler aborted")
3992        ));
3993    }
3994
3995    #[test]
3996    fn proxy_helper_paths_cover_remaining_validation_and_headers() {
3997        assert!(matches!(
3998            super::validate_proxy_header_name("Host"),
3999            Err(crate::NanoGetError::ProtocolManagedHeader(_))
4000        ));
4001        assert!(matches!(
4002            super::validate_proxy_header_name("TE"),
4003            Err(crate::NanoGetError::HopByHopHeader(_))
4004        ));
4005
4006        let proxy = ProxyConfig::new("http://127.0.0.1:8080").unwrap();
4007        let request = Request::get("https://example.com").unwrap();
4008        let config = ClientConfig {
4009            preemptive_proxy_authorization: Some("Basic cHJveHk6c2VjcmV0".to_string()),
4010            ..ClientConfig::default()
4011        };
4012        let headers = prepared_connect_headers(&request, &config, &proxy).unwrap();
4013        assert!(headers
4014            .iter()
4015            .any(|header| header.matches_name("proxy-authorization")));
4016
4017        let mut request_with_proxy = Request::get("https://example.com").unwrap();
4018        request_with_proxy
4019            .proxy_authorization("Basic explicit")
4020            .unwrap();
4021        let headers = prepared_connect_headers(&request_with_proxy, &config, &proxy).unwrap();
4022        assert!(headers
4023            .iter()
4024            .any(|header| header.value() == "Basic explicit"));
4025
4026        let mut cache = MemoryCache::default();
4027        let get_request = Request::get("http://example.com/head").unwrap();
4028        let get_response = response(
4029            200,
4030            vec![
4031                Header::unchecked("Cache-Control", "max-age=60"),
4032                Header::unchecked("ETag", "\"v1\""),
4033                Header::unchecked("Content-Length", "6"),
4034            ],
4035            b"abcdef",
4036        );
4037        cache.store(
4038            &get_request,
4039            &timed_response(get_response),
4040            &AuthContext::default(),
4041        );
4042        let head_request = Request::head("http://example.com/head").unwrap();
4043        let head_response = response(
4044            200,
4045            vec![
4046                Header::unchecked("Cache-Control", "max-age=60"),
4047                Header::unchecked("ETag", "\"v2\""),
4048                Header::unchecked("Content-Length", "8"),
4049            ],
4050            b"",
4051        );
4052        cache.store(
4053            &head_request,
4054            &timed_response(head_response),
4055            &AuthContext::default(),
4056        );
4057    }
4058
4059    #[cfg(not(feature = "https"))]
4060    #[test]
4061    fn open_https_tunnel_reports_feature_required_after_connect_success() {
4062        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4063        let port = listener.local_addr().unwrap().port();
4064        let handle = thread::spawn(move || {
4065            let (mut stream, _) = listener.accept().unwrap();
4066            let mut request = Vec::new();
4067            let mut chunk = [0u8; 256];
4068            loop {
4069                let read = stream.read(&mut chunk).unwrap();
4070                assert!(read > 0, "client closed before sending CONNECT request");
4071                request.extend_from_slice(&chunk[..read]);
4072                if request.windows(4).any(|window| window == b"\r\n\r\n") {
4073                    break;
4074                }
4075            }
4076            stream
4077                .write_all(b"HTTP/1.1 200 Connection Established\r\nContent-Length: 0\r\n\r\n")
4078                .unwrap();
4079        });
4080
4081        let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4082        let config = ClientConfig::default();
4083        let request = Request::get("https://example.com").unwrap();
4084        let error = super::open_https_tunnel(&config, &request, &proxy)
4085            .err()
4086            .expect("expected tunnel failure");
4087        assert!(matches!(error, crate::NanoGetError::HttpsFeatureRequired));
4088        handle.join().unwrap();
4089    }
4090
4091    #[cfg(not(feature = "https"))]
4092    #[test]
4093    fn open_https_tunnel_without_auth_handler_returns_proxy_connect_failed() {
4094        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4095        let port = listener.local_addr().unwrap().port();
4096        let handle = thread::spawn(move || {
4097            let (mut stream, _) = listener.accept().unwrap();
4098            let mut request = Vec::new();
4099            let mut chunk = [0u8; 256];
4100            loop {
4101                let read = stream.read(&mut chunk).unwrap();
4102                assert!(read > 0, "client closed before sending CONNECT request");
4103                request.extend_from_slice(&chunk[..read]);
4104                if request.windows(4).any(|window| window == b"\r\n\r\n") {
4105                    break;
4106                }
4107            }
4108            stream
4109                .write_all(
4110                    b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
4111                )
4112                .unwrap();
4113        });
4114
4115        let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4116        let request = Request::get("https://example.com").unwrap();
4117        let error = super::open_https_tunnel(&ClientConfig::default(), &request, &proxy)
4118            .err()
4119            .expect("expected proxy failure");
4120        assert!(matches!(
4121            error,
4122            crate::NanoGetError::ProxyConnectFailed(407, _)
4123        ));
4124        handle.join().unwrap();
4125    }
4126
4127    #[cfg(not(feature = "https"))]
4128    #[test]
4129    fn open_https_tunnel_non_auth_proxy_failure_is_reported() {
4130        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4131        let port = listener.local_addr().unwrap().port();
4132        let handle = thread::spawn(move || {
4133            let (mut stream, _) = listener.accept().unwrap();
4134            let mut request = Vec::new();
4135            let mut chunk = [0u8; 256];
4136            loop {
4137                let read = stream.read(&mut chunk).unwrap();
4138                assert!(read > 0, "client closed before sending CONNECT request");
4139                request.extend_from_slice(&chunk[..read]);
4140                if request.windows(4).any(|window| window == b"\r\n\r\n") {
4141                    break;
4142                }
4143            }
4144            stream
4145                .write_all(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
4146                .unwrap();
4147        });
4148
4149        let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4150        let request = Request::get("https://example.com").unwrap();
4151        let error = super::open_https_tunnel(&ClientConfig::default(), &request, &proxy)
4152            .err()
4153            .expect("expected proxy failure");
4154        assert!(matches!(
4155            error,
4156            crate::NanoGetError::ProxyConnectFailed(502, _)
4157        ));
4158        handle.join().unwrap();
4159    }
4160
4161    #[cfg(feature = "https")]
4162    #[test]
4163    fn open_https_tunnel_returns_proxy_error_without_handler() {
4164        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4165        let port = listener.local_addr().unwrap().port();
4166        let handle = thread::spawn(move || {
4167            let (mut stream, _) = listener.accept().unwrap();
4168            let mut request = Vec::new();
4169            let mut chunk = [0u8; 256];
4170            loop {
4171                let read = stream.read(&mut chunk).unwrap();
4172                assert!(read > 0, "client closed before sending CONNECT request");
4173                request.extend_from_slice(&chunk[..read]);
4174                if request.windows(4).any(|window| window == b"\r\n\r\n") {
4175                    break;
4176                }
4177            }
4178            stream
4179                .write_all(
4180                    b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
4181                )
4182                .unwrap();
4183        });
4184
4185        let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4186        let request = Request::get("https://example.com").unwrap();
4187        let error = super::open_https_tunnel(&ClientConfig::default(), &request, &proxy)
4188            .err()
4189            .expect("expected tunnel failure");
4190        assert!(matches!(
4191            error,
4192            crate::NanoGetError::ProxyConnectFailed(407, _)
4193        ));
4194        handle.join().unwrap();
4195    }
4196
4197    #[cfg(not(feature = "https"))]
4198    #[test]
4199    fn open_https_tunnel_retries_proxy_auth_challenges() {
4200        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4201        let port = listener.local_addr().unwrap().port();
4202        let requests = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
4203        let requests_for_thread = Arc::clone(&requests);
4204        let handle = thread::spawn(move || {
4205            for index in 0..2 {
4206                let (mut stream, _) = listener.accept().unwrap();
4207                let mut request = Vec::new();
4208                let mut chunk = [0u8; 256];
4209                let mut read = stream.read(&mut chunk).unwrap();
4210                while read > 0 {
4211                    request.extend_from_slice(&chunk[..read]);
4212                    if request.windows(4).any(|window| window == b"\r\n\r\n") {
4213                        break;
4214                    }
4215                    read = stream.read(&mut chunk).unwrap();
4216                }
4217                requests_for_thread
4218                    .lock()
4219                    .unwrap()
4220                    .push(String::from_utf8_lossy(&request).into_owned());
4221                if index == 0 {
4222                    stream
4223                        .write_all(b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n")
4224                        .unwrap();
4225                } else {
4226                    stream
4227                        .write_all(
4228                            b"HTTP/1.1 200 Connection Established\r\nContent-Length: 0\r\n\r\n",
4229                        )
4230                        .unwrap();
4231                }
4232            }
4233        });
4234
4235        let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4236        let config = ClientConfig {
4237            proxy_auth_handler: Some(Arc::new(super::BasicAuthHandler::new(
4238                "proxy",
4239                "secret",
4240                AuthTarget::Proxy,
4241            ))),
4242            ..ClientConfig::default()
4243        };
4244        let request = Request::get("https://example.com").unwrap();
4245        let error = super::open_https_tunnel(&config, &request, &proxy)
4246            .err()
4247            .expect("expected tunnel failure");
4248        assert!(matches!(error, crate::NanoGetError::HttpsFeatureRequired));
4249
4250        let captured = requests.lock().unwrap().clone();
4251        assert_eq!(captured.len(), 2);
4252        assert!(!captured[0].contains("Proxy-Authorization:"));
4253        assert!(captured[1].contains("Proxy-Authorization: Basic"));
4254        handle.join().unwrap();
4255    }
4256
4257    #[cfg(feature = "https")]
4258    #[test]
4259    fn open_https_tunnel_retries_proxy_auth_before_reporting_failure() {
4260        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
4261        let port = listener.local_addr().unwrap().port();
4262        let requests = Arc::new(Mutex::new(Vec::<String>::new()));
4263        let requests_for_thread = Arc::clone(&requests);
4264        let handle = thread::spawn(move || {
4265            for index in 0..2 {
4266                let (mut stream, _) = listener.accept().unwrap();
4267                let mut request = Vec::new();
4268                let mut chunk = [0u8; 256];
4269                let mut read = stream.read(&mut chunk).unwrap();
4270                while read > 0 {
4271                    request.extend_from_slice(&chunk[..read]);
4272                    if request.windows(4).any(|window| window == b"\r\n\r\n") {
4273                        break;
4274                    }
4275                    read = stream.read(&mut chunk).unwrap();
4276                }
4277                requests_for_thread
4278                    .lock()
4279                    .unwrap()
4280                    .push(String::from_utf8_lossy(&request).into_owned());
4281                if index == 0 {
4282                    stream
4283                        .write_all(
4284                            b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
4285                        )
4286                        .unwrap();
4287                } else {
4288                    stream
4289                        .write_all(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
4290                        .unwrap();
4291                }
4292            }
4293        });
4294
4295        let proxy = ProxyConfig::new(format!("http://127.0.0.1:{port}")).unwrap();
4296        let config = ClientConfig {
4297            proxy_auth_handler: Some(Arc::new(super::BasicAuthHandler::new(
4298                "proxy",
4299                "secret",
4300                AuthTarget::Proxy,
4301            ))),
4302            ..ClientConfig::default()
4303        };
4304        let request = Request::get("https://example.com").unwrap();
4305        let error = super::open_https_tunnel(&config, &request, &proxy)
4306            .err()
4307            .expect("expected https feature error");
4308        assert!(matches!(
4309            error,
4310            crate::NanoGetError::ProxyConnectFailed(502, _)
4311        ));
4312
4313        let captured = requests.lock().unwrap().clone();
4314        assert_eq!(captured.len(), 2);
4315        assert!(!captured[0].contains("Proxy-Authorization:"));
4316        assert!(captured[1].contains("Proxy-Authorization: Basic"));
4317        handle.join().unwrap();
4318    }
4319}