Skip to main content

net/
http_loader.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashSet;
6use std::iter::FromIterator;
7use std::sync::Arc as StdArc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::{Duration, SystemTime};
10
11use async_recursion::async_recursion;
12use content_security_policy::percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
13use devtools_traits::ChromeToDevtoolsControlMsg;
14use embedder_traits::{AuthenticationResponse, GenericEmbedderProxy};
15use futures::{TryFutureExt, TryStreamExt, future};
16use headers::authorization::Basic;
17use headers::{
18    AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods,
19    AccessControlMaxAge, AccessControlRequestMethod, Authorization, CacheControl, ContentLength,
20    HeaderMapExt, IfModifiedSince, LastModified, Pragma, Referer, StrictTransportSecurity,
21    UserAgent,
22};
23use http::header::{
24    self, ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION,
25    CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LOCATION, CONTENT_TYPE, HeaderValue, RANGE,
26    WWW_AUTHENTICATE,
27};
28use http::{HeaderMap, Method, Request as HyperRequest, StatusCode};
29use http_body_util::combinators::BoxBody;
30use http_body_util::{BodyExt, Full};
31use hyper::Response as HyperResponse;
32use hyper::body::{Bytes, Frame};
33use hyper::ext::ReasonPhrase;
34use hyper::header::{HeaderName, TRANSFER_ENCODING};
35use ipc_channel::IpcError;
36use ipc_channel::ipc::{self, IpcSender};
37use ipc_channel::router::ROUTER;
38use log::{debug, error, info, log_enabled, warn};
39use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
40use net_traits::blob_url_store::UrlWithBlobClaim;
41use net_traits::fetch::headers::get_value_from_header_list;
42use net_traits::http_status::HttpStatus;
43use net_traits::policy_container::{EmbedderPolicyValue, RequestPolicyContainer};
44use net_traits::pub_domains::{is_same_site, reg_suffix};
45use net_traits::request::{
46    BodyChunkRequest, BodyChunkResponse, CacheMode, CredentialsMode, Destination, Initiator,
47    Origin, RedirectMode, Referrer, Request, RequestBuilder, RequestClient, RequestMode,
48    ResponseTainting, ServiceWorkersMode, TraversableForUserPrompts, get_cors_unsafe_header_names,
49    is_cors_non_wildcard_request_header_name, is_cors_safelisted_method,
50    is_cors_safelisted_request_header,
51};
52use net_traits::response::{CacheState, RedirectTaint, Response, ResponseBody, ResponseType};
53use net_traits::{
54    CookieSource, DOCUMENT_ACCEPT_HEADER_VALUE, DiscardFetch, NetworkError, RedirectEndValue,
55    RedirectStartValue, ReferrerPolicy, ResourceAttribute, ResourceFetchTimingContainer,
56    ResourceTimeValue, ServoCipherSuite, ServoNamedGroup, ServoProtocolVersion, TlsSecurityInfo,
57    TlsSecurityState,
58};
59use parking_lot::{Mutex, RwLock};
60use profile_traits::mem::{Report, ReportKind};
61use profile_traits::path;
62#[cfg(feature = "tracing")]
63use profile_traits::trace_span;
64use rustc_hash::FxHashMap;
65use servo_base::cross_process_instant::CrossProcessInstant;
66use servo_base::generic_channel::GenericSharedMemory;
67use servo_base::id::{BrowsingContextId, HistoryStateId, PipelineId};
68use servo_config::pref;
69use servo_url::{ImmutableOrigin, ServoUrl};
70use tokio::sync::mpsc::{
71    Receiver as TokioReceiver, Sender as TokioSender, UnboundedReceiver, UnboundedSender, channel,
72    unbounded_channel,
73};
74use tokio_stream::wrappers::ReceiverStream;
75#[cfg(feature = "tracing")]
76use tracing::Instrument;
77
78use crate::async_runtime::spawn_task;
79use crate::connector::{
80    CertificateErrorOverrideManager, ServoClient, TlsHandshakeInfo, create_tls_config,
81};
82use crate::cookie::ServoCookie;
83use crate::cookie_storage::CookieStorage;
84use crate::decoder::Decoder;
85use crate::devtools::{
86    prepare_devtools_request, send_request_to_devtools, send_response_values_to_devtools,
87};
88use crate::embedder::NetToEmbedderMsg;
89use crate::fetch::cors_cache::CorsCache;
90use crate::fetch::fetch_params::FetchParams;
91use crate::fetch::headers::{SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser};
92use crate::fetch::methods::{Data, DoneChannel, FetchContext, Target, fetch, main_fetch};
93use crate::hsts::HstsList;
94use crate::http_cache::{
95    CacheKey, CachedResourcesOrGuard, HttpCache, ValidationStatus, construct_response,
96    invalidate_cached_resources, refresh,
97};
98use crate::resource_thread::{AuthCache, AuthCacheEntry};
99use crate::websocket_loader::start_websocket;
100
101/// The various states an entry of the HttpCache can be in.
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub enum HttpCacheEntryState {
104    /// The entry is fully up-to-date,
105    /// there are no pending concurrent stores,
106    /// and it is ready to construct cached responses.
107    ReadyToConstruct,
108    /// The entry is pending a number of concurrent stores.
109    PendingStore(usize),
110}
111
112pub struct HttpState {
113    pub hsts_list: RwLock<HstsList>,
114    pub cookie_jar: RwLock<CookieStorage>,
115    pub http_cache: HttpCache,
116    pub auth_cache: RwLock<AuthCache>,
117    pub history_states: RwLock<FxHashMap<HistoryStateId, Vec<u8>>>,
118    pub client: ServoClient,
119    pub override_manager: CertificateErrorOverrideManager,
120    pub embedder_proxy: GenericEmbedderProxy<NetToEmbedderMsg>,
121}
122
123impl HttpState {
124    pub(crate) fn memory_reports(&self, suffix: &str, ops: &mut MallocSizeOfOps) -> Vec<Report> {
125        vec![
126            Report {
127                path: path!["memory-cache", suffix],
128                kind: ReportKind::ExplicitJemallocHeapSize,
129                size: self.http_cache.size_of(ops),
130            },
131            Report {
132                path: path!["hsts-list", suffix],
133                kind: ReportKind::ExplicitJemallocHeapSize,
134                size: self.hsts_list.read().size_of(ops),
135            },
136            Report {
137                path: path!["auth cache", suffix],
138                kind: ReportKind::ExplicitJemallocHeapSize,
139                size: self.auth_cache.read().size_of(ops),
140            },
141            Report {
142                path: path!["cookie storage", suffix],
143                kind: ReportKind::ExplicitJemallocHeapSize,
144                size: self.cookie_jar.read().size_of(ops),
145            },
146        ]
147    }
148
149    async fn request_authentication(
150        &self,
151        request: &Request,
152        response: &Response,
153    ) -> Option<AuthenticationResponse> {
154        // We do not make an authentication request for non-WebView associated HTTP requests.
155        let webview_id = request.target_webview_id?;
156        let for_proxy = response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED;
157
158        // If this is not actually a navigation request return None.
159        if request.mode != RequestMode::Navigate {
160            return None;
161        }
162
163        let (sender, receiver) = tokio::sync::oneshot::channel();
164        self.embedder_proxy
165            .send(NetToEmbedderMsg::RequestAuthentication(
166                webview_id,
167                request.url(),
168                for_proxy,
169                sender,
170            ));
171        receiver.await.ok()?
172    }
173}
174
175/// Step 11 of <https://fetch.spec.whatwg.org/#concept-fetch>.
176pub(crate) fn set_default_accept(request: &mut Request) {
177    // Step 11. If request’s header list does not contain `Accept`, then:
178    if request.headers.contains_key(header::ACCEPT) {
179        return;
180    }
181
182    // Step 11.2. If request’s initiator is "prefetch", then set value to the document `Accept` header value.
183    let value = if request.initiator == Initiator::Prefetch {
184        DOCUMENT_ACCEPT_HEADER_VALUE
185    } else {
186        // Step 11.3. Otherwise, the user agent should set value to the first matching statement,
187        // if any, switching on request’s destination:
188        match request.destination {
189            Destination::Document | Destination::Frame | Destination::IFrame => {
190                DOCUMENT_ACCEPT_HEADER_VALUE
191            },
192            Destination::Image => {
193                HeaderValue::from_static("image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5")
194            },
195            Destination::Json => HeaderValue::from_static("application/json,*/*;q=0.5"),
196            Destination::Style => HeaderValue::from_static("text/css,*/*;q=0.1"),
197            Destination::Text => HeaderValue::from_static("text/plain,*/*;q=0.5"),
198            // Step 11.1. Let value be `*/*`.
199            _ => HeaderValue::from_static("*/*"),
200        }
201    };
202
203    // Step 11.4. Append (`Accept`, value) to request’s header list.
204    request.headers.insert(header::ACCEPT, value);
205}
206
207fn set_default_accept_encoding(headers: &mut HeaderMap) {
208    if headers.contains_key(header::ACCEPT_ENCODING) {
209        return;
210    }
211
212    // TODO(eijebong): Change this once typed headers are done
213    headers.insert(
214        header::ACCEPT_ENCODING,
215        HeaderValue::from_static("gzip, deflate, br, zstd"),
216    );
217}
218
219/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-state-no-referrer-when-downgrade>
220fn no_referrer_when_downgrade(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
221    // Step 1
222    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
223        return None;
224    }
225    // Step 2
226    strip_url_for_use_as_referrer(referrer_url, false)
227}
228
229/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin>
230fn strict_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
231    // Step 1
232    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
233        return None;
234    }
235    // Step 2
236    strip_url_for_use_as_referrer(referrer_url, true)
237}
238
239/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin-when-cross-origin>
240fn strict_origin_when_cross_origin(
241    referrer_url: ServoUrl,
242    current_url: ServoUrl,
243) -> Option<ServoUrl> {
244    // Step 1
245    if referrer_url.origin() == current_url.origin() {
246        return strip_url_for_use_as_referrer(referrer_url, false);
247    }
248    // Step 2
249    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
250        return None;
251    }
252    // Step 3
253    strip_url_for_use_as_referrer(referrer_url, true)
254}
255
256/// <https://html.spec.whatwg.org/multipage/#schemelessly-same-site>
257fn is_schemelessy_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
258    // Step 1
259    if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b {
260        true
261    } else if site_a.is_tuple() && site_b.is_tuple() {
262        // Step 2.1
263        let host_a = site_a.host().map(|h| h.to_string()).unwrap_or_default();
264        let host_b = site_b.host().map(|h| h.to_string()).unwrap_or_default();
265
266        let host_a_reg = reg_suffix(&host_a);
267        let host_b_reg = reg_suffix(&host_b);
268
269        // Step 2.2-2.3
270        (site_a.host() == site_b.host() && host_a_reg.is_empty()) ||
271            (host_a_reg == host_b_reg && !host_a_reg.is_empty())
272    } else {
273        // Step 3
274        false
275    }
276}
277
278/// <https://w3c.github.io/webappsec-referrer-policy/#strip-url>
279fn strip_url_for_use_as_referrer(mut url: ServoUrl, origin_only: bool) -> Option<ServoUrl> {
280    const MAX_REFERRER_URL_LENGTH: usize = 4096;
281    // Step 2
282    if url.is_local_scheme() {
283        return None;
284    }
285    // Step 3-6
286    {
287        let url = url.as_mut_url();
288        let _ = url.set_username("");
289        let _ = url.set_password(None);
290        url.set_fragment(None);
291        // Note: The result of serializing referrer url should not be
292        // greater than 4096 as specified in Step 6 of
293        // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
294        if origin_only || url.as_str().len() > MAX_REFERRER_URL_LENGTH {
295            url.set_path("");
296            url.set_query(None);
297        }
298    }
299    // Step 7
300    Some(url)
301}
302
303/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-same-origin>
304fn same_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
305    // Step 1
306    if referrer_url.origin() == current_url.origin() {
307        return strip_url_for_use_as_referrer(referrer_url, false);
308    }
309    // Step 2
310    None
311}
312
313/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-origin-when-cross-origin>
314fn origin_when_cross_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
315    // Step 1
316    if referrer_url.origin() == current_url.origin() {
317        return strip_url_for_use_as_referrer(referrer_url, false);
318    }
319    // Step 2
320    strip_url_for_use_as_referrer(referrer_url, true)
321}
322
323/// <https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer>
324pub fn determine_requests_referrer(
325    referrer_policy: ReferrerPolicy,
326    referrer_source: ServoUrl,
327    current_url: ServoUrl,
328) -> Option<ServoUrl> {
329    match referrer_policy {
330        ReferrerPolicy::EmptyString | ReferrerPolicy::NoReferrer => None,
331        ReferrerPolicy::Origin => strip_url_for_use_as_referrer(referrer_source, true),
332        ReferrerPolicy::UnsafeUrl => strip_url_for_use_as_referrer(referrer_source, false),
333        ReferrerPolicy::StrictOrigin => strict_origin(referrer_source, current_url),
334        ReferrerPolicy::StrictOriginWhenCrossOrigin => {
335            strict_origin_when_cross_origin(referrer_source, current_url)
336        },
337        ReferrerPolicy::SameOrigin => same_origin(referrer_source, current_url),
338        ReferrerPolicy::OriginWhenCrossOrigin => {
339            origin_when_cross_origin(referrer_source, current_url)
340        },
341        ReferrerPolicy::NoReferrerWhenDowngrade => {
342            no_referrer_when_downgrade(referrer_source, current_url)
343        },
344    }
345}
346
347fn set_request_cookies(
348    url: &ServoUrl,
349    headers: &mut HeaderMap,
350    cookie_jar: &RwLock<CookieStorage>,
351) {
352    let mut cookie_jar = cookie_jar.write();
353    cookie_jar.remove_expired_cookies_for_url(url);
354    if let Some(cookie_list) = cookie_jar.cookies_for_url(url, CookieSource::HTTP) &&
355        let Ok(cookie_list_header_value) = HeaderValue::from_bytes(cookie_list.as_bytes())
356    {
357        headers.insert(header::COOKIE, cookie_list_header_value);
358    }
359}
360
361fn set_cookie_for_url(cookie_jar: &RwLock<CookieStorage>, request: &ServoUrl, cookie_val: &str) {
362    let mut cookie_jar = cookie_jar.write();
363    let source = CookieSource::HTTP;
364
365    if let Some(cookie) = ServoCookie::from_cookie_string(cookie_val, request, source) {
366        cookie_jar.push(cookie, request, source);
367    }
368}
369
370fn set_cookies_from_headers(
371    url: &ServoUrl,
372    headers: &HeaderMap,
373    cookie_jar: &RwLock<CookieStorage>,
374) {
375    for cookie in headers.get_all(header::SET_COOKIE) {
376        let cookie_bytes = cookie.as_bytes();
377        if !ServoCookie::is_valid_name_or_value(cookie_bytes) {
378            continue;
379        }
380        if let Ok(cookie_str) = std::str::from_utf8(cookie_bytes) {
381            set_cookie_for_url(cookie_jar, url, cookie_str);
382        }
383    }
384}
385
386fn build_tls_security_info(handshake: &TlsHandshakeInfo, hsts_enabled: bool) -> TlsSecurityInfo {
387    // Simplified security state determination:
388    // Servo uses rustls, which only supports TLS 1.2+ and secure cipher suites (GCM, ChaCha20-Poly1305).
389    // rustls does NOT support TLS 1.0, TLS 1.1, SSL, or weak ciphers (RC4, 3DES, CBC, etc).
390    // Therefore, any successful TLS connection is secure by design.
391    //
392    // We only check for missing handshake information as a defensive measure.
393
394    let state = if handshake.protocol_version.is_none() || handshake.cipher_suite.is_none() {
395        // Missing handshake information indicates an incomplete or failed connection
396        TlsSecurityState::Insecure
397    } else {
398        // rustls guarantees TLS 1.2+ with secure ciphers
399        TlsSecurityState::Secure
400    };
401
402    TlsSecurityInfo {
403        state,
404        weakness_reasons: Vec::new(), // rustls never negotiates weak crypto
405        protocol_version: handshake.protocol_version.map(ServoProtocolVersion),
406        cipher_suite: handshake.cipher_suite.map(ServoCipherSuite),
407        kea_group_name: handshake.kea_group_name.map(ServoNamedGroup),
408        signature_scheme_name: handshake.signature_scheme_name.clone(),
409        alpn_protocol: handshake.alpn_protocol.clone(),
410        certificate_chain_der: handshake.certificate_chain_der.clone(),
411        certificate_transparency: None,
412        hsts: hsts_enabled,
413        hpkp: false,
414        used_ech: handshake.used_ech,
415        used_delegated_credentials: false,
416        used_ocsp: false,
417        used_private_dns: false,
418    }
419}
420
421fn auth_from_cache(
422    auth_cache: &RwLock<AuthCache>,
423    origin: &ImmutableOrigin,
424) -> Option<Authorization<Basic>> {
425    if let Some(auth_entry) = auth_cache
426        .read()
427        .entries
428        .get(origin.ascii_serialization().as_ref())
429    {
430        let user_name = &auth_entry.user_name;
431        let password = &auth_entry.password;
432        Some(Authorization::basic(user_name, password))
433    } else {
434        None
435    }
436}
437
438/// Messages from the IPC route to the fetch worker,
439/// used to fill the body with bytes coming-in over IPC.
440enum BodyChunk {
441    /// A chunk of bytes.
442    Chunk(GenericSharedMemory),
443    /// Body is done.
444    Done,
445}
446
447/// The stream side of the body passed to hyper.
448enum BodyStream {
449    /// A receiver that can be used in Body::wrap_stream,
450    /// for streaming the request over the network.
451    Chunked(TokioReceiver<Result<Frame<Bytes>, hyper::Error>>),
452    /// A body whose bytes are buffered
453    /// and sent in one chunk over the network.
454    Buffered(UnboundedReceiver<BodyChunk>),
455}
456
457/// The sink side of the body passed to hyper,
458/// used to enqueue chunks.
459enum BodySink {
460    /// A Tokio sender used to feed chunks to the network stream.
461    Chunked(TokioSender<Result<Frame<Bytes>, hyper::Error>>),
462    /// A Crossbeam sender used to send chunks to the fetch worker,
463    /// where they will be buffered
464    /// in order to ensure they are not streamed them over the network.
465    Buffered(UnboundedSender<BodyChunk>),
466}
467
468impl BodySink {
469    fn transmit_bytes(&self, bytes: GenericSharedMemory) {
470        match self {
471            BodySink::Chunked(sender) => {
472                let sender = sender.clone();
473                spawn_task(async move {
474                    let _ = sender
475                        .send(Ok(Frame::data(Bytes::copy_from_slice(&bytes))))
476                        .await;
477                });
478            },
479            BodySink::Buffered(sender) => {
480                let _ = sender.send(BodyChunk::Chunk(bytes));
481            },
482        }
483    }
484
485    fn close(self) {
486        match self {
487            BodySink::Chunked(_) => {},
488            BodySink::Buffered(sender) => {
489                let _ = sender.send(BodyChunk::Done);
490            },
491        }
492    }
493}
494
495fn request_body_stream_closed_error(action: &str) -> NetworkError {
496    NetworkError::Crash(format!(
497        "Request body stream has already been closed while trying to {action}."
498    ))
499}
500
501fn log_request_body_stream_closed(action: &str, error: Option<&IpcError>) {
502    match error {
503        Some(error) => {
504            error!("Request body stream has already been closed while trying to {action}: {error}")
505        },
506        None => error!("Request body stream has already been closed while trying to {action}."),
507    }
508}
509
510fn log_fetch_terminated_send_failure(terminated_with_error: bool, context: &str) {
511    warn!(
512        "Failed to notify request-body stream termination state ({terminated_with_error}) while {context} because the receiver was already dropped."
513    );
514}
515
516const FRAGMENT: &AsciiSet = &CONTROLS.add(b'|').add(b'{').add(b'}');
517
518#[allow(clippy::too_many_arguments)]
519#[servo_tracing::instrument(skip_all, fields(url=url.as_str()))]
520/// This sets up the callback infrastructure to send body frames to `body_sender` and fires the client request.
521async fn obtain_response(
522    client: &ServoClient,
523    url: &ServoUrl,
524    method: &Method,
525    request_headers: &mut HeaderMap,
526    body_sender: Option<StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>>,
527    source_is_null: bool,
528    pipeline_id: &Option<PipelineId>,
529    request_id: Option<&str>,
530    destination: Destination,
531    is_xhr: bool,
532    context: &FetchContext,
533    fetch_terminated: UnboundedSender<bool>,
534    browsing_context_id: Option<BrowsingContextId>,
535) -> Result<(HyperResponse<Decoder>, Option<ChromeToDevtoolsControlMsg>), NetworkError> {
536    let mut headers = request_headers.clone();
537
538    let devtools_bytes = StdArc::new(Mutex::new(vec![]));
539
540    // https://url.spec.whatwg.org/#percent-encoded-bytes
541    let encoded_url = utf8_percent_encode(url.as_str(), FRAGMENT).to_string();
542
543    let request = if let Some(chunk_requester) = body_sender {
544        let (sink, stream) = if source_is_null {
545            // Step 4.2 of https://fetch.spec.whatwg.org/#concept-http-network-fetch
546            // TODO: this should not be set for HTTP/2(currently not supported?).
547            headers.insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
548
549            let (sender, receiver) = channel(1);
550            (BodySink::Chunked(sender), BodyStream::Chunked(receiver))
551        } else {
552            // Note: Hyper seems to already buffer bytes when the request appears not stream-able,
553            // see https://github.com/hyperium/hyper/issues/2232#issuecomment-644322104
554            //
555            // However since this doesn't appear documented, and we're using an ancient version,
556            // for now we buffer manually to ensure we don't stream requests
557            // to servers that might not know how to handle them.
558            let (sender, receiver) = unbounded_channel();
559            (BodySink::Buffered(sender), BodyStream::Buffered(receiver))
560        };
561
562        obtain_response_setup_router_callback(
563            devtools_bytes.clone(),
564            chunk_requester,
565            sink,
566            fetch_terminated,
567        )?;
568
569        let body = match stream {
570            BodyStream::Chunked(receiver) => {
571                let stream = ReceiverStream::new(receiver);
572                BoxBody::new(http_body_util::StreamBody::new(stream))
573            },
574            BodyStream::Buffered(mut receiver) => {
575                // Accumulate bytes received over IPC into a vector.
576                let mut body = vec![];
577                loop {
578                    match receiver.recv().await {
579                        Some(BodyChunk::Chunk(bytes)) => {
580                            body.extend_from_slice(&bytes);
581                        },
582                        Some(BodyChunk::Done) => break,
583                        None => warn!("Failed to read all chunks from request body."),
584                    }
585                }
586                Full::new(body.into()).map_err(|_| unreachable!()).boxed()
587            },
588        };
589        HyperRequest::builder()
590            .method(method)
591            .uri(encoded_url)
592            .body(body)
593    } else {
594        HyperRequest::builder()
595            .method(method)
596            .uri(encoded_url)
597            .body(
598                http_body_util::Empty::new()
599                    .map_err(|_| unreachable!())
600                    .boxed(),
601            )
602    };
603
604    // TODO(#21261) connect_start: set if a persistent connection is *not* used and the last non-redirected
605    // fetch passes the timing allow check
606    let connect_start = CrossProcessInstant::now();
607    context.timing.set_attributes(&[
608        ResourceAttribute::DomainLookupStart,
609        ResourceAttribute::ConnectStart(connect_start),
610    ]);
611
612    // TODO: We currently don't know when the handhhake before the connection is done
613    // so our best bet would be to set `secure_connection_start` here when we are currently
614    // fetching on a HTTPS url.
615    if url.scheme() == "https" {
616        context
617            .timing
618            .set_attribute(ResourceAttribute::SecureConnectionStart);
619    }
620
621    let mut request = match request {
622        Ok(request) => request,
623        Err(error) => return Err(NetworkError::HttpError(error.to_string())),
624    };
625    *request.headers_mut() = headers.clone();
626
627    let connect_end = CrossProcessInstant::now();
628    context
629        .timing
630        .set_attribute(ResourceAttribute::ConnectEnd(connect_end));
631
632    let request_id = request_id.map(|v| v.to_owned());
633    let pipeline_id = *pipeline_id;
634    let closure_url = url.clone();
635    let method = method.clone();
636    let send_start = CrossProcessInstant::now();
637
638    let host = request.uri().host().unwrap_or("").to_owned();
639    let override_manager = context.state.override_manager.clone();
640    let headers = headers.clone();
641    let is_secure_scheme = url.is_secure_scheme();
642
643    // Generally, we use a persistent connection, so we will also set other PerformanceResourceTiming
644    //   attributes to this as well (domain_lookup_start, domain_lookup_end, connect_start, connect_end,
645    //   secure_connection_start)
646    context
647        .timing
648        .set_attribute(ResourceAttribute::RequestStart);
649
650    let client_future = client
651        .request(request)
652        .and_then(move |res| {
653            let send_end = CrossProcessInstant::now();
654
655            // TODO(#21271) response_start: immediately after receiving first byte of response
656
657            let msg = if let Some(request_id) = request_id {
658                if let Some(pipeline_id) = pipeline_id {
659                    if let Some(browsing_context_id) = browsing_context_id {
660                        Some(prepare_devtools_request(
661                            request_id,
662                            closure_url,
663                            method.clone(),
664                            headers,
665                            Some(devtools_bytes.lock().clone()),
666                            pipeline_id,
667                            (connect_end - connect_start).unsigned_abs(),
668                            (send_end - send_start).unsigned_abs(),
669                            destination,
670                            is_xhr,
671                            browsing_context_id,
672                        ))
673                    } else {
674                        debug!("Not notifying devtools (no browsing_context_id)");
675                        None
676                    }
677                    // TODO: ^This is not right, connect_start is taken before contructing the
678                    // request and connect_end at the end of it. send_start is takend before the
679                    // connection too. I'm not sure it's currently possible to get the time at the
680                    // point between the connection and the start of a request.
681                } else {
682                    debug!("Not notifying devtools (no pipeline_id)");
683                    None
684                }
685            } else {
686                debug!("Not notifying devtools (no request_id)");
687                None
688            };
689
690            future::ready(Ok((
691                Decoder::detect(res.map(|r| r.boxed()), is_secure_scheme),
692                msg,
693            )))
694        })
695        .map_err(move |error| {
696            warn!("network error: {error:?}");
697            NetworkError::from_hyper_error(
698                &error,
699                override_manager.remove_certificate_failing_verification(host.as_str()),
700            )
701        });
702
703    #[cfg(feature = "tracing")]
704    {
705        client_future.instrument(trace_span!("HyperRequest")).await
706    }
707
708    #[cfg(not(feature = "tracing"))]
709    {
710        client_future.await
711    }
712}
713
714/// Setup the callback mechanism to forward chunks from the request received to the `chunk_requester`.
715fn obtain_response_setup_router_callback(
716    devtools_bytes: StdArc<Mutex<Vec<u8>>>,
717    chunk_requester: StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>,
718    sink: BodySink,
719    fetch_terminated: UnboundedSender<bool>,
720) -> Result<(), NetworkError> {
721    let (body_chan, body_port) = ipc::channel().unwrap();
722
723    {
724        let mut lock = chunk_requester.lock();
725        if let Some(chunk_requester) = lock.as_mut() {
726            if let Err(error) = chunk_requester.send(BodyChunkRequest::Connect(body_chan)) {
727                log_request_body_stream_closed("connect to the request body stream", Some(&error));
728                return Err(request_body_stream_closed_error(
729                    "connect to the request body stream",
730                ));
731            }
732
733            // https://fetch.spec.whatwg.org/#concept-request-transmit-body
734            // Request the first chunk, corresponding to Step 3 and 4.
735            if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
736                log_request_body_stream_closed(
737                    "request the first request body chunk",
738                    Some(&error),
739                );
740                return Err(request_body_stream_closed_error(
741                    "request the first request body chunk",
742                ));
743            }
744        } else {
745            log_request_body_stream_closed("connect to the request body stream", None);
746            return Err(request_body_stream_closed_error(
747                "connect to the request body stream",
748            ));
749        }
750    }
751
752    let mut sink = Some(sink);
753
754    ROUTER.add_typed_route(
755        body_port,
756        Box::new(move |message| {
757            info!("Received message");
758            let bytes = match message.unwrap() {
759                BodyChunkResponse::Chunk(bytes) => bytes,
760                BodyChunkResponse::Done => {
761                    // Step 2.2.2. If fetchParams’s process request end-of-body is non-null,
762                    // then run fetchParams’s process request end-of-body.
763                    if fetch_terminated.send(false).is_err() {
764                        log_fetch_terminated_send_failure(
765                            false,
766                            "handling request body completion",
767                        );
768                    }
769                    if let Some(sink) = sink.take() {
770                        sink.close();
771                    }
772
773                    return;
774                },
775                BodyChunkResponse::Error => {
776                    // Step 4 and/or 5.
777                    // TODO: differentiate between the two steps,
778                    // where step 5 requires setting an `aborted` flag on the fetch.
779                    if fetch_terminated.send(true).is_err() {
780                        log_fetch_terminated_send_failure(
781                            true,
782                            "handling request body stream error",
783                        );
784                    }
785                    if let Some(sink) = sink.take() {
786                        sink.close();
787                    }
788
789                    return;
790                },
791            };
792
793            devtools_bytes.lock().extend_from_slice(&bytes);
794
795            // Step 5.1.2.2, transmit chunk over the network,
796            // currently implemented by sending the bytes to the fetch worker.
797            {
798                let Some(sink) = sink.as_ref() else {
799                    return;
800                };
801                sink.transmit_bytes(bytes);
802            }
803
804            // Step 5.1.2.3
805            // Request the next chunk.
806            let mut chunk_requester = chunk_requester.lock();
807            if let Some(chunk_requester) = chunk_requester.as_mut() {
808                if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
809                    log_request_body_stream_closed(
810                        "request the next request body chunk",
811                        Some(&error),
812                    );
813                    if fetch_terminated.send(true).is_err() {
814                        log_fetch_terminated_send_failure(
815                            true,
816                            "handling failure to request the next request body chunk",
817                        );
818                    }
819                    if let Some(sink) = sink.take() {
820                        sink.close();
821                    }
822                }
823            } else {
824                log_request_body_stream_closed("request the next request body chunk", None);
825                if fetch_terminated.send(true).is_err() {
826                    log_fetch_terminated_send_failure(
827                        true,
828                        "handling a closed request body stream while requesting the next chunk",
829                    );
830                }
831                if let Some(sink) = sink.take() {
832                    sink.close();
833                }
834            }
835        }),
836    );
837
838    Ok(())
839}
840
841/// [HTTP fetch](https://fetch.spec.whatwg.org/#concept-http-fetch)
842#[async_recursion]
843#[allow(clippy::too_many_arguments)]
844pub(crate) async fn http_fetch(
845    fetch_params: &mut FetchParams,
846    cache: &mut CorsCache,
847    cors_flag: bool,
848    cors_preflight_flag: bool,
849    authentication_fetch_flag: bool,
850    target: Target<'async_recursion>,
851    done_chan: &mut DoneChannel,
852    context: &FetchContext,
853) -> Response {
854    // This is a new async fetch, reset the channel we are waiting on
855    *done_chan = None;
856    // Step 1. Let request be fetchParams’s request.
857    let request = &mut fetch_params.request;
858
859    // Step 2. Let response and internalResponse be null.
860    let mut response: Option<Response> = None;
861
862    // Step 3. If request’s service-workers mode is "all", then
863    if request.service_workers_mode == ServiceWorkersMode::All {
864        // TODO: Substep 1
865        // Set response to the result of invoking handle fetch for request.
866
867        // Substep 2
868        if let Some(ref res) = response {
869            // Subsubstep 1
870            // TODO: transmit body for request
871
872            // Subsubstep 2
873            // nothing to do, since actual_response is a function on response
874
875            // Subsubstep 3
876            if (res.response_type == ResponseType::Opaque && request.mode != RequestMode::NoCors) ||
877                (res.response_type == ResponseType::OpaqueRedirect &&
878                    request.redirect_mode != RedirectMode::Manual) ||
879                (res.url_list.len() > 1 && request.redirect_mode != RedirectMode::Follow) ||
880                res.is_network_error()
881            {
882                return Response::network_error(NetworkError::ConnectionFailure);
883            }
884
885            // Subsubstep 4
886            // TODO: set response's CSP list on actual_response
887        }
888    }
889
890    // Step 4. If response is null, then:
891    if response.is_none() {
892        // Step 4.1. If makeCORSPreflight is true and one of these conditions is true:
893        if cors_preflight_flag {
894            let method_cache_match = cache.match_method(request, request.method.clone());
895
896            // There is no method cache entry match for request’s method using request, and either
897            // request’s method is not a CORS-safelisted method or request’s use-CORS-preflight flag
898            // is set.
899            let method_mismatch = !method_cache_match &&
900                (!is_cors_safelisted_method(&request.method) || request.use_cors_preflight);
901
902            // There is at least one item in the CORS-unsafe request-header names with request’s
903            // header list for which there is no header-name cache entry match using request.
904            let header_mismatch = request.headers.iter().any(|(name, value)| {
905                !cache.match_header(request, name) &&
906                    !is_cors_safelisted_request_header(&name, &value)
907            });
908
909            // Then:
910            if method_mismatch || header_mismatch {
911                // Step 4.1.1. Let preflightResponse be the result of running
912                // CORS-preflight fetch given request.
913                let preflight_response = cors_preflight_fetch(request, cache, context).await;
914                // Step 4.1.2. If preflightResponse is a network error, then return preflightResponse.
915                if let Some(error) = preflight_response.get_network_error() {
916                    return Response::network_error(error.clone());
917                }
918            }
919        }
920
921        // Step 4.2. If request’s redirect mode is "follow",
922        // then set request’s service-workers mode to "none".
923        if request.redirect_mode == RedirectMode::Follow {
924            request.service_workers_mode = ServiceWorkersMode::None;
925        }
926
927        // Step 4.3. Set response and internalResponse to the result of
928        // running HTTP-network-or-cache fetch given fetchParams.
929        let mut fetch_result = http_network_or_cache_fetch(
930            fetch_params,
931            authentication_fetch_flag,
932            cors_flag,
933            done_chan,
934            context,
935        )
936        .await;
937
938        // Step 4.4. If request’s response tainting is "cors" and a CORS check for request
939        // and response returns failure, then return a network error.
940        if cors_flag && cors_check(&fetch_params.request, &fetch_result).is_err() {
941            return Response::network_error(NetworkError::CorsGeneral);
942        }
943
944        // Step 4.5. If the TAO check for request and response returns failure,
945        // then set request’s timing allow failed flag.
946        if let Err(()) = tao_check(&fetch_params.request, &fetch_result) {
947            context.timing.inner().mark_timing_check_failed();
948        }
949        fetch_result.return_internal = false;
950        response = Some(fetch_result);
951    }
952
953    let request = &mut fetch_params.request;
954
955    // response is guaranteed to be something by now
956    let mut response = response.unwrap();
957
958    // Step 5: If either request’s response tainting or response’s type is "opaque",
959    // and the cross-origin resource policy check with request’s origin, request’s client,
960    // request’s destination, and internalResponse returns blocked, then return a network error.
961    if (request.response_tainting == ResponseTainting::Opaque ||
962        response.response_type == ResponseType::Opaque) &&
963        request.client.as_ref().is_some_and(|client| {
964            cross_origin_resource_policy_check(
965                &request.origin,
966                client,
967                &response,
968                ForNavigation::No,
969            ) == CrossOriginResourcePolicy::Blocked
970        })
971    {
972        return Response::network_error(NetworkError::CrossOriginResponse);
973    }
974
975    // Step 6. If internalResponse’s status is a redirect status:
976    if response
977        .actual_response()
978        .status
979        .try_code()
980        .is_some_and(is_redirect_status)
981    {
982        // TODO Step 6.1. If request is a navigation request, then append to a request’s navigation
983        // timing allow values list given request and internalResponse.
984
985        // Step 6.2. If internalResponse’s status is not 303, request’s body is non-null,
986        // and the connection uses HTTP/2, then user agents may, and are even encouraged to,
987        // transmit an RST_STREAM frame.
988        if response.actual_response().status != StatusCode::SEE_OTHER {
989            // TODO: send RST_STREAM frame
990        }
991
992        // Step 6.3. Switch on request’s redirect mode:
993        response = match request.redirect_mode {
994            // Step 6.3."error".1. Set response to a network error.
995            RedirectMode::Error => Response::network_error(NetworkError::RedirectError),
996            RedirectMode::Manual => {
997                // Step 6.3."manual".1. If request’s mode is "navigate", then set fetchParams’s controller’s
998                // next manual redirect steps to run HTTP-redirect fetch given fetchParams and response.
999                if request.mode == RequestMode::Navigate {
1000                    // TODO: We don't implement Fetch controller. Instead, we update the location url
1001                    // of the response here and don't call `http_redirect_fetch`. That's get called later.
1002                    // Once we have a fetch controller here, we should update the code as specced.
1003                    let location_url =
1004                        location_url_for_response(&response, request.current_url().fragment());
1005                    response.actual_response_mut().location_url = location_url;
1006                    response
1007                } else {
1008                    // Step 6.3."manual".2. Otherwise, set response to an opaque-redirect filtered
1009                    // response whose internal response is internalResponse.
1010                    response.to_filtered(ResponseType::OpaqueRedirect)
1011                }
1012            },
1013            RedirectMode::Follow => {
1014                // TODO Step 6.3."follow".1. Run the WebDriver BiDi response completed steps with
1015                // request and response.
1016                // set back to default
1017                response.return_internal = true;
1018
1019                // Step 6.3."follow".2. Set response to the result of running HTTP-redirect fetch
1020                // given fetchParams and response.
1021                http_redirect_fetch(
1022                    fetch_params,
1023                    cache,
1024                    response,
1025                    cors_flag,
1026                    target,
1027                    done_chan,
1028                    context,
1029                )
1030                .await
1031            },
1032        };
1033    }
1034
1035    // set back to default
1036    response.return_internal = true;
1037    context
1038        .timing
1039        .set_attribute(ResourceAttribute::RedirectCount(
1040            fetch_params.request.redirect_count as u16,
1041        ));
1042
1043    response.resource_timing = context.timing.clone();
1044
1045    // Step 7. Return response
1046    response
1047}
1048
1049/// <https://fetch.spec.whatwg.org/#concept-tao-check>
1050fn tao_check(request: &Request, response: &Response) -> Result<(), ()> {
1051    // Step 1. Assert: request’s origin is not "client".
1052    let Origin::Origin(ref request_origin) = request.origin else {
1053        unreachable!("origin cannot be \"client\" at this point");
1054    };
1055
1056    // Step 2. If request’s timing allow failed flag is set, then return failure.
1057
1058    // Step 3. Let values be the result of getting, decoding, and splitting `Timing-Allow-Origin`
1059    // from response’s header list.
1060    let values: Vec<&str> = response
1061        .headers
1062        .get_all("Timing-Allow-Origin")
1063        .iter()
1064        .map(|header_value| header_value.to_str().unwrap_or(""))
1065        .collect();
1066
1067    // Step 4. If values contains "*", then return success.
1068    if values.contains(&"*") {
1069        return Ok(());
1070    }
1071
1072    // Step 5. If values contains the result of serializing a request origin with request, then
1073    // return success.
1074    if values
1075        .iter()
1076        .any(|header_str| *header_str == request_origin.ascii_serialization().as_ref())
1077    {
1078        return Ok(());
1079    }
1080
1081    // Step 6. If request’s mode is "navigate" and request’s current URL’s origin is not same origin
1082    // with request’s origin, then return failure.
1083    if request.mode == RequestMode::Navigate && request.current_url().origin() != *request_origin {
1084        return Err(());
1085    }
1086
1087    // Step 7. If request’s response tainting is "basic", then return success.
1088    if request.response_tainting == ResponseTainting::Basic {
1089        return Ok(());
1090    }
1091
1092    // Step 8. Return failure.
1093    Err(())
1094}
1095
1096// Convenience struct that implements Drop, for setting redirectEnd on function return
1097struct RedirectEndTimer(Option<ResourceFetchTimingContainer>);
1098
1099impl RedirectEndTimer {
1100    fn neuter(&mut self) {
1101        self.0 = None;
1102    }
1103}
1104
1105impl Drop for RedirectEndTimer {
1106    fn drop(&mut self) {
1107        let RedirectEndTimer(resource_fetch_timing_opt) = self;
1108
1109        resource_fetch_timing_opt.as_ref().map_or((), |t| {
1110            t.set_attribute(ResourceAttribute::RedirectEnd(RedirectEndValue::Zero));
1111        })
1112    }
1113}
1114
1115/// <https://fetch.spec.whatwg.org/#request-body-header-name>
1116static REQUEST_BODY_HEADER_NAMES: &[HeaderName] = &[
1117    CONTENT_ENCODING,
1118    CONTENT_LANGUAGE,
1119    CONTENT_LOCATION,
1120    CONTENT_TYPE,
1121];
1122
1123/// <https://fetch.spec.whatwg.org/#concept-response-location-url>
1124fn location_url_for_response(
1125    response: &Response,
1126    request_fragment: Option<&str>,
1127) -> Option<Result<ServoUrl, String>> {
1128    // Step 1. If response’s status is not a redirect status, then return null.
1129    assert!(
1130        response
1131            .actual_response()
1132            .status
1133            .try_code()
1134            .is_some_and(is_redirect_status)
1135    );
1136    // Step 2. Let location be the result of extracting header list values given `Location` and response’s header list.
1137    let mut location = response
1138        .actual_response()
1139        .headers
1140        .get(header::LOCATION)
1141        .and_then(|header_value| {
1142            HeaderValue::to_str(header_value)
1143                .map(|location_string| {
1144                    // Step 3. If location is a header value, then set location to the result of parsing location with response’s URL.
1145                    ServoUrl::parse_with_base(response.actual_response().url(), location_string)
1146                        .map_err(|error| error.to_string())
1147                })
1148                .ok()
1149        });
1150
1151    // Step 4. If location is a URL whose fragment is null, then set location’s fragment to requestFragment.
1152    if let Some(Ok(ref mut location)) = location &&
1153        location.fragment().is_none()
1154    {
1155        location.set_fragment(request_fragment);
1156    }
1157    // Step 5. Return location.
1158    location
1159}
1160
1161/// [HTTP redirect fetch](https://fetch.spec.whatwg.org/#http-redirect-fetch)
1162#[async_recursion]
1163pub async fn http_redirect_fetch(
1164    fetch_params: &mut FetchParams,
1165    cache: &mut CorsCache,
1166    mut response: Response,
1167    cors_flag: bool,
1168    target: Target<'async_recursion>,
1169    done_chan: &mut DoneChannel,
1170    context: &FetchContext,
1171) -> Response {
1172    let mut redirect_end_timer = RedirectEndTimer(Some(context.timing.clone()));
1173
1174    // Step 1. Let request be fetchParams’s request.
1175    let request = &mut fetch_params.request;
1176
1177    // Step 2. Let internalResponse be response, if response is not a filtered response; otherwise response’s internal response.
1178    assert!(response.return_internal);
1179
1180    // Step 3. Let locationURL be internalResponse’s location URL given request’s current URL’s fragment.
1181    let location_url = location_url_for_response(&response, request.current_url().fragment());
1182    response.actual_response_mut().location_url = location_url.clone();
1183
1184    let location_url = match location_url {
1185        // Step 4. If locationURL is null, then return response.
1186        None => return response,
1187        // Step 5. If locationURL is failure, then return a network error.
1188        Some(Err(err)) => {
1189            return Response::network_error(NetworkError::ResourceLoadError(
1190                "Location URL parse failure: ".to_owned() + &err,
1191            ));
1192        },
1193        // Step 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network error.
1194        Some(Ok(url)) if !matches!(url.scheme(), "http" | "https") => {
1195            return Response::network_error(NetworkError::UnsupportedScheme);
1196        },
1197        Some(Ok(url)) => url,
1198    };
1199
1200    // Step 1 of https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-fetchstart
1201    // TODO: check origin and timing allow check
1202    // start_time should equal redirect_start if nonzero; else fetch_start
1203    // updates start_time only if redirect_start is nonzero (implying TAO)
1204    context.timing.set_attributes(&[
1205        ResourceAttribute::RedirectStart(RedirectStartValue::FetchStart),
1206        ResourceAttribute::FetchStart,
1207        ResourceAttribute::StartTime(ResourceTimeValue::FetchStart),
1208        ResourceAttribute::StartTime(ResourceTimeValue::RedirectStart),
1209    ]);
1210
1211    // Step 7: If request’s redirect count is 20, then return a network error.
1212    if request.redirect_count >= 20 {
1213        return Response::network_error(NetworkError::TooManyRedirects);
1214    }
1215
1216    // Step 8: Increase request’s redirect count by 1.
1217    request.redirect_count += 1;
1218
1219    // Step 9. If request’s mode is "cors", locationURL includes credentials,
1220    // and request’s origin is not same origin with locationURL’s origin, then return a network error.
1221    let same_origin = match request.origin {
1222        Origin::Origin(ref origin) => *origin == location_url.origin(),
1223        Origin::Client => panic!(
1224            "Request origin should not be client for {}",
1225            request.current_url()
1226        ),
1227    };
1228
1229    let has_credentials = has_credentials(&location_url);
1230
1231    if request.mode == RequestMode::CorsMode && !same_origin && has_credentials {
1232        return Response::network_error(NetworkError::CorsCredentials);
1233    }
1234
1235    if cors_flag && location_url.origin() != request.current_url().origin() {
1236        request.origin = Origin::Origin(ImmutableOrigin::new_opaque());
1237    }
1238
1239    // Step 10. If request’s response tainting is "cors" and locationURL includes credentials, then return a network error.
1240    if cors_flag && has_credentials {
1241        return Response::network_error(NetworkError::CorsCredentials);
1242    }
1243
1244    // Step 11: If internalResponse’s status is not 303, request’s body is non-null, and request’s
1245    // body’s source is null, then return a network error.
1246    if response.actual_response().status != StatusCode::SEE_OTHER &&
1247        request.body.as_ref().is_some_and(|b| b.source_is_null())
1248    {
1249        return Response::network_error(NetworkError::ConnectionFailure);
1250    }
1251
1252    // Step 12. If one of the following is true
1253    if response
1254        .actual_response()
1255        .status
1256        .try_code()
1257        .is_some_and(|code| {
1258            // internalResponse’s status is 301 or 302 and request’s method is `POST`
1259            ((code == StatusCode::MOVED_PERMANENTLY || code == StatusCode::FOUND) &&
1260                request.method == Method::POST) ||
1261                // internalResponse’s status is 303 and request’s method is not `GET` or `HEAD`
1262                (code == StatusCode::SEE_OTHER &&
1263                    request.method != Method::HEAD &&
1264                    request.method != Method::GET)
1265        })
1266    {
1267        // Step 12.1. Set request’s method to `GET` and request’s body to null.
1268        request.method = Method::GET;
1269        request.body = None;
1270        // Step 12.2. For each headerName of request-body-header name, delete headerName from request’s header list.
1271        for name in REQUEST_BODY_HEADER_NAMES {
1272            request.headers.remove(name);
1273        }
1274    }
1275
1276    // Step 13: If request’s current URL’s origin is not same origin with locationURL’s origin, then
1277    // for each headerName of CORS non-wildcard request-header name, delete headerName from
1278    // request’s header list.
1279    if location_url.origin() != request.current_url().origin() {
1280        // This list currently only contains the AUTHORIZATION header
1281        // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
1282        request.headers.remove(AUTHORIZATION);
1283    }
1284
1285    // Step 14: If request’s body is non-null, then set request’s body to the body of the result of
1286    // safely extracting request’s body’s source.
1287    if let Some(body) = request.body.as_mut() {
1288        body.extract_source();
1289    }
1290
1291    // Steps 15-17 relate to timing, which is not implemented 1:1 with the spec.
1292
1293    // Step 18: Append locationURL to request’s URL list.
1294    request
1295        .url_list
1296        .push(UrlWithBlobClaim::from_url_without_having_claimed_blob(
1297            location_url,
1298        ));
1299
1300    // Step 19: Invoke set request’s referrer policy on redirect on request and internalResponse.
1301    set_requests_referrer_policy_on_redirect(request, response.actual_response());
1302
1303    // Step 20: Let recursive be true.
1304    // Step 21: If request’s redirect mode is "manual", then...
1305    let recursive_flag = request.redirect_mode != RedirectMode::Manual;
1306
1307    // Step 22: Return the result of running main fetch given fetchParams and recursive.
1308    let fetch_response = main_fetch(
1309        fetch_params,
1310        cache,
1311        recursive_flag,
1312        target,
1313        done_chan,
1314        context,
1315    )
1316    .await;
1317
1318    // TODO: timing allow check
1319    context.timing.set_attribute(ResourceAttribute::RedirectEnd(
1320        RedirectEndValue::ResponseEnd,
1321    ));
1322    redirect_end_timer.neuter();
1323
1324    fetch_response
1325}
1326
1327/// [HTTP network or cache fetch](https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch)
1328#[async_recursion]
1329#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
1330async fn http_network_or_cache_fetch(
1331    fetch_params: &mut FetchParams,
1332    authentication_fetch_flag: bool,
1333    cors_flag: bool,
1334    done_chan: &mut DoneChannel,
1335    context: &FetchContext,
1336) -> Response {
1337    // Step 2. Let httpFetchParams be null.
1338    let http_fetch_params: &mut FetchParams;
1339    let mut fetch_params_copy: FetchParams;
1340
1341    // Step 3. Let httpRequest be null. (See step 8 for initialization)
1342
1343    // Step 4. Let response be null.
1344    let mut response: Option<Response> = None;
1345
1346    // Step 7. Let the revalidatingFlag be unset.
1347    let mut revalidating_flag = false;
1348
1349    // TODO(#33616): Step 8. Run these steps, but abort when fetchParams is canceled:
1350    // Step 8.1. If request’s traversable for user prompts is "no-traversable"
1351    // and request’s redirect mode is "error", then set httpFetchParams to fetchParams and httpRequest to request.
1352    let http_request = if fetch_params.request.traversable_for_user_prompts ==
1353        TraversableForUserPrompts::NoTraversable &&
1354        fetch_params.request.redirect_mode == RedirectMode::Error
1355    {
1356        http_fetch_params = fetch_params;
1357        &mut http_fetch_params.request
1358    }
1359    // Step 8.2 Otherwise:
1360    else {
1361        // Step 8.2.1 - 8.2.3: Set httpRequest to a clone of request
1362        // and Set httpFetchParams to a copy of fetchParams.
1363        fetch_params_copy =
1364            std::mem::replace(fetch_params, FetchParams::new(fetch_params.request.clone()));
1365        http_fetch_params = &mut fetch_params_copy;
1366
1367        &mut http_fetch_params.request
1368    };
1369
1370    // Step 8.3: Let includeCredentials be true if one of:
1371    let include_credentials = match http_request.credentials_mode {
1372        // request’s credentials mode is "include"
1373        CredentialsMode::Include => true,
1374        // request’s credentials mode is "same-origin" and request’s response tainting is "basic"
1375        CredentialsMode::CredentialsSameOrigin
1376            if http_request.response_tainting == ResponseTainting::Basic =>
1377        {
1378            true
1379        },
1380        _ => false,
1381    };
1382
1383    // Step 8.4: If Cross-Origin-Embedder-Policy allows credentials with request returns false, then
1384    // set includeCredentials to false.
1385    // TODO(#33616): Requires request's client object
1386
1387    // Step 8.5 Let contentLength be httpRequest’s body’s length, if httpRequest’s body is non-null;
1388    // otherwise null.
1389    let content_length = http_request
1390        .body
1391        .as_ref()
1392        .and_then(|body| body.len().map(|size| size as u64));
1393
1394    // Step 8.6 Let contentLengthHeaderValue be null.
1395    let mut content_length_header_value = None;
1396
1397    // Step 8.7 If httpRequest’s body is null and httpRequest’s method is `POST` or `PUT`,
1398    // then set contentLengthHeaderValue to `0`.
1399    if http_request.body.is_none() && matches!(http_request.method, Method::POST | Method::PUT) {
1400        content_length_header_value = Some(0);
1401    }
1402
1403    // Step 8.8 If contentLength is non-null, then set contentLengthHeaderValue to contentLength,
1404    // serialized and isomorphic encoded.
1405    // NOTE: The header will later be serialized using HeaderMap::typed_insert
1406    if let Some(content_length) = content_length {
1407        content_length_header_value = Some(content_length);
1408    };
1409
1410    // Step 8.9 If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue)
1411    // to httpRequest’s header list.
1412    if let Some(content_length_header_value) = content_length_header_value {
1413        http_request
1414            .headers
1415            .typed_insert(ContentLength(content_length_header_value));
1416    }
1417
1418    // Step 8.10 If contentLength is non-null and httpRequest’s keepalive is true, then:
1419    if http_request.keep_alive &&
1420        let Some(content_length) = content_length
1421    {
1422        // Step 8.10.1. Let inflightKeepaliveBytes be 0.
1423        // Step 8.10.2. Let group be httpRequest’s client’s fetch group.
1424        // Step 8.10.3. Let inflightRecords be the set of fetch records
1425        // in group whose request’s keepalive is true and done flag is unset.
1426        let in_flight_keep_alive_bytes: u64 = context
1427            .in_flight_keep_alive_records
1428            .lock()
1429            .get(
1430                &http_request
1431                    .pipeline_id
1432                    .expect("Must always set a pipeline ID for keep-alive requests"),
1433            )
1434            .map(|records| {
1435                // Step 8.10.4. For each fetchRecord of inflightRecords:
1436                // Step 8.10.4.1. Let inflightRequest be fetchRecord’s request.
1437                // Step 8.10.4.2. Increment inflightKeepaliveBytes by inflightRequest’s body’s length.
1438                records
1439                    .iter()
1440                    .map(|record| {
1441                        if record.request_id == http_request.id {
1442                            // Don't double count for this request. We have already added it in
1443                            // `fetch::methods::fetch_with_cors_cache`
1444                            0
1445                        } else {
1446                            record.keep_alive_body_length
1447                        }
1448                    })
1449                    .sum()
1450            })
1451            .unwrap_or_default();
1452        // Step 8.10.5. If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
1453        if content_length + in_flight_keep_alive_bytes > 64 * 1024 {
1454            return Response::network_error(NetworkError::TooManyInFlightKeepAliveRequests);
1455        }
1456    }
1457
1458    // Step 8.11: If httpRequest’s referrer is a URL, then:
1459    match http_request.referrer {
1460        Referrer::ReferrerUrl(ref http_request_referrer) |
1461        Referrer::Client(ref http_request_referrer) => {
1462            // Step 8.11.1: Let referrerValue be httpRequest’s referrer, serialized and isomorphic
1463            // encoded.
1464            if let Ok(referer) = http_request_referrer.as_str().parse::<Referer>() {
1465                // Step 8.11.2: Append (`Referer`, referrerValue) to httpRequest’s header list.
1466                http_request.headers.typed_insert(referer);
1467            } else {
1468                // This error should only happen in cases where hyper and rust-url disagree
1469                // about how to parse a referer.
1470                // https://github.com/servo/servo/issues/24175
1471                error!("Failed to parse {} as referrer", http_request_referrer);
1472            }
1473        },
1474        _ => {},
1475    };
1476
1477    // Step 8.12 Append a request `Origin` header for httpRequest.
1478    append_a_request_origin_header(http_request);
1479
1480    // Step 8.13 Append the Fetch metadata headers for httpRequest.
1481    append_the_fetch_metadata_headers(http_request);
1482
1483    // Step 8.14: If httpRequest’s initiator is "prefetch", then set a structured field value given
1484    // (`Sec-Purpose`, the token "prefetch") in httpRequest’s header list.
1485    if http_request.initiator == Initiator::Prefetch &&
1486        let Ok(value) = HeaderValue::from_str("prefetch")
1487    {
1488        http_request.headers.insert("Sec-Purpose", value);
1489    }
1490
1491    // Step 8.15: If httpRequest’s header list does not contain `User-Agent`, then user agents
1492    // should append (`User-Agent`, default `User-Agent` value) to httpRequest’s header list.
1493    if !http_request.headers.contains_key(header::USER_AGENT) {
1494        http_request
1495            .headers
1496            .typed_insert::<UserAgent>(context.user_agent.parse().unwrap());
1497    }
1498
1499    // Steps 8.16 to 8.18
1500    append_cache_data_to_headers(http_request);
1501
1502    // Step 8.19: If httpRequest’s header list contains `Range`, then append (`Accept-Encoding`,
1503    // `identity`) to httpRequest’s header list.
1504    if http_request.headers.contains_key(header::RANGE) &&
1505        let Ok(value) = HeaderValue::from_str("identity")
1506    {
1507        http_request.headers.insert("Accept-Encoding", value);
1508    }
1509
1510    // Step 8.20: Modify httpRequest’s header list per HTTP. Do not append a given header if
1511    // httpRequest’s header list contains that header’s name.
1512    // `Accept`, `Accept-Charset`, and `Accept-Language` must not be included at this point.
1513    http_request.headers.remove(header::HOST);
1514    // unlike http_loader, we should not set the accept header here
1515    set_default_accept_encoding(&mut http_request.headers);
1516
1517    let current_url = http_request.current_url();
1518
1519    // Step 8.21: If includeCredentials is true, then:
1520    // TODO some of this step can't be implemented yet
1521    if include_credentials {
1522        // Substep 1
1523        // TODO http://mxr.mozilla.org/servo/source/components/net/http_loader.rs#504
1524        // XXXManishearth http_loader has block_cookies: support content blocking here too
1525        set_request_cookies(
1526            &current_url,
1527            &mut http_request.headers,
1528            &context.state.cookie_jar,
1529        );
1530        // Substep 2
1531        if !http_request.headers.contains_key(header::AUTHORIZATION) {
1532            // Substep 3
1533            let mut authorization_value = None;
1534
1535            // Substep 4
1536            if let Some(basic) = auth_from_cache(&context.state.auth_cache, &current_url.origin()) &&
1537                (!http_request.use_url_credentials || !has_credentials(&current_url))
1538            {
1539                authorization_value = Some(basic);
1540            }
1541
1542            // Substep 5
1543            if authentication_fetch_flag &&
1544                authorization_value.is_none() &&
1545                has_credentials(&current_url)
1546            {
1547                authorization_value = Some(Authorization::basic(
1548                    current_url.username(),
1549                    current_url.password().unwrap_or(""),
1550                ));
1551            }
1552
1553            // Substep 6
1554            if let Some(basic) = authorization_value {
1555                http_request.headers.typed_insert(basic);
1556            }
1557        }
1558    }
1559
1560    // TODO(#33616) Step 8.22 If there’s a proxy-authentication entry, use it as appropriate.
1561    let should_wait = {
1562        // Enter critical section on cache entry.
1563        let mut cache_guard = block_for_cache_ready(
1564            context,
1565            http_request,
1566            done_chan,
1567            &mut revalidating_flag,
1568            &mut response,
1569        )
1570        .await;
1571
1572        // TODO(#33616): Step 9. If aborted, then return the appropriate network error for fetchParams.
1573
1574        // Step 10. If response is null, then:
1575        if response.is_none() {
1576            // Step 10.1 If httpRequest’s cache mode is "only-if-cached", then return a network error.
1577            if http_request.cache_mode == CacheMode::OnlyIfCached {
1578                // Exit critical section of cache entry.
1579                return Response::network_error(NetworkError::CacheError);
1580            }
1581
1582            // Step 10.2 Let forwardResponse be the result of running HTTP-network fetch given httpFetchParams,
1583            // includeCredentials, and isNewConnectionFetch.
1584            drop(cache_guard);
1585            let forward_response =
1586                http_network_fetch(http_fetch_params, include_credentials, done_chan, context)
1587                    .await;
1588
1589            let http_request = &mut http_fetch_params.request;
1590            let request_key = CacheKey::new(http_request);
1591            cache_guard = context
1592                .state
1593                .http_cache
1594                .get_or_guard(request_key.clone())
1595                .await;
1596            // Step 10.3 If httpRequest’s method is unsafe and forwardResponse’s status is in the range 200 to 399,
1597            // inclusive, invalidate appropriate stored responses in httpCache, as per the
1598            // "Invalidating Stored Responses" chapter of HTTP Caching, and set storedResponse to null.
1599            if forward_response.status.in_range(200..=399) && !http_request.method.is_safe() {
1600                if let Some(guard) = cache_guard.try_as_mut() {
1601                    invalidate_cached_resources(guard);
1602                }
1603                context
1604                    .state
1605                    .http_cache
1606                    .invalidate_related_urls(http_request, &forward_response, &request_key)
1607                    .await;
1608            }
1609
1610            // Step 10.4 If the revalidatingFlag is set and forwardResponse’s status is 304, then:
1611            if revalidating_flag && forward_response.status == StatusCode::NOT_MODIFIED {
1612                // Ensure done_chan is None,
1613                // since the network response will be replaced by the revalidated stored one.
1614                *done_chan = None;
1615                if let Some(guard) = cache_guard.try_as_mut() {
1616                    response = refresh(http_request, forward_response.clone(), done_chan, guard);
1617                }
1618
1619                if let Some(response) = &mut response {
1620                    response.cache_state = CacheState::Validated;
1621                }
1622            }
1623
1624            // Step 10.5 If response is null, then:
1625            if response.is_none() {
1626                // Step 10.5.1 Set response to forwardResponse.
1627                let forward_response = response.insert(forward_response);
1628
1629                // Per https://httpwg.org/specs/rfc9111.html#response.cacheability we must not cache responses
1630                // if the No-Store directive is present
1631                if http_request.cache_mode != CacheMode::NoStore {
1632                    // Step 10.5.2 Store httpRequest and forwardResponse in httpCache, as per the
1633                    //             "Storing Responses in Caches" chapter of HTTP Caching.
1634                    cache_guard.insert(http_request, forward_response);
1635                }
1636            }
1637            false
1638        } else {
1639            true
1640        }
1641    }; // Exit Critical Section on cache entry
1642
1643    if should_wait {
1644        // If the cache constructed a response, and that is still receiving from the network,
1645        // we must wait for it to finish in case it is still receiving from the network.
1646        // Note: this means only the fetch from which the original network response originated
1647        // will be able to stream it; all others receive a cached response in one chunk.
1648        wait_for_inflight_requests(done_chan, &mut response).await;
1649    }
1650
1651    let http_request = &mut http_fetch_params.request;
1652    let mut response = response.unwrap();
1653
1654    // Step 11. Set response’s URL list to a clone of httpRequest’s URL list.
1655    response.url_list = http_request
1656        .url_list
1657        .iter()
1658        .map(|claimed_url| claimed_url.url())
1659        .collect();
1660
1661    // Step 12. If httpRequest’s header list contains `Range`, then set response’s range-requested flag.
1662    if http_request.headers.contains_key(RANGE) {
1663        response.range_requested = true;
1664    }
1665
1666    // Step 13. Set response’s request-includes-credentials to includeCredentials.
1667    response.request_includes_credentials = include_credentials;
1668
1669    // Step 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
1670    // includeCredentials is true, and request’s window is an environment settings object, then:
1671    // TODO(#33616): Figure out what to do with request window objects
1672    // NOTE: Requiring a WWW-Authenticate header here is ad-hoc, but seems to match what other browsers are
1673    // doing. See Step 14.1.
1674    if response.status.try_code() == Some(StatusCode::UNAUTHORIZED) &&
1675        !cors_flag &&
1676        include_credentials &&
1677        response.headers.contains_key(WWW_AUTHENTICATE)
1678    {
1679        // TODO: Step 14.1 Spec says requires testing on multiple WWW-Authenticate headers
1680
1681        let request = &mut fetch_params.request;
1682
1683        // Step 14.2 If request’s body is non-null, then:
1684        // “If request’s body’s source is null, then return a network error.”
1685        if request
1686            .body
1687            .as_ref()
1688            .is_some_and(|body| body.source_is_null())
1689        {
1690            return Response::network_error(NetworkError::ConnectionFailure);
1691        }
1692
1693        // Step 14.3 If request’s use-URL-credentials flag is unset or isAuthenticationFetch is true, then:
1694        if !request.use_url_credentials || authentication_fetch_flag {
1695            let Some(credentials) = context
1696                .state
1697                .request_authentication(request, &response)
1698                .await
1699            else {
1700                return response;
1701            };
1702
1703            if let Err(err) = request
1704                .current_url_mut()
1705                .set_username(&credentials.username)
1706            {
1707                error!("error setting username for url: {:?}", err);
1708                return response;
1709            };
1710
1711            if let Err(err) = request
1712                .current_url_mut()
1713                .set_password(Some(&credentials.password))
1714            {
1715                error!("error setting password for url: {:?}", err);
1716                return response;
1717            };
1718        }
1719
1720        // Make sure this is set to None,
1721        // since we're about to start a new `http_network_or_cache_fetch`.
1722        *done_chan = None;
1723
1724        // Step 14.4 Set response to the result of running HTTP-network-or-cache fetch given fetchParams and true.
1725        response = http_network_or_cache_fetch(
1726            fetch_params,
1727            true, /* authentication flag */
1728            cors_flag,
1729            done_chan,
1730            context,
1731        )
1732        .await;
1733    }
1734
1735    // Step 15. If response’s status is 407, then:
1736    if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
1737        let request = &mut fetch_params.request;
1738        // Step 15.1 If request’s traversable for user prompts is "no-traversable", then return a network error.
1739
1740        if request.traversable_for_user_prompts == TraversableForUserPrompts::NoTraversable {
1741            return Response::network_error(NetworkError::ResourceLoadError(
1742                "Can't find Window object".into(),
1743            ));
1744        }
1745
1746        // (Step 15.2 does not exist, requires testing on Proxy-Authenticate headers)
1747
1748        // TODO(#33616): Step 15.3 If fetchParams is canceled, then return
1749        // the appropriate network error for fetchParams.
1750
1751        // Step 15.4 Prompt the end user as appropriate in request’s window
1752        // window and store the result as a proxy-authentication entry.
1753        let Some(credentials) = context
1754            .state
1755            .request_authentication(request, &response)
1756            .await
1757        else {
1758            return response;
1759        };
1760
1761        // Store the credentials as a proxy-authentication entry.
1762        let entry = AuthCacheEntry {
1763            user_name: credentials.username,
1764            password: credentials.password,
1765        };
1766        {
1767            let mut auth_cache = context.state.auth_cache.write();
1768            let key = request
1769                .current_url()
1770                .origin()
1771                .ascii_serialization()
1772                .into_owned();
1773            auth_cache.entries.insert(key, entry);
1774        }
1775
1776        // Make sure this is set to None,
1777        // since we're about to start a new `http_network_or_cache_fetch`.
1778        *done_chan = None;
1779
1780        // Step 15.5 Set response to the result of running HTTP-network-or-cache fetch given fetchParams.
1781        response = http_network_or_cache_fetch(
1782            fetch_params,
1783            false, /* authentication flag */
1784            cors_flag,
1785            done_chan,
1786            context,
1787        )
1788        .await;
1789    }
1790
1791    // TODO(#33616): Step 16. If all of the following are true:
1792    // * response’s status is 421
1793    // * isNewConnectionFetch is false
1794    // * request’s body is null, or request’s body is non-null and request’s body’s source is non-null
1795    // then: [..]
1796
1797    // Step 17. If isAuthenticationFetch is true, then create an authentication entry for request and the given realm.
1798    if authentication_fetch_flag {
1799        // TODO(#33616)
1800    }
1801
1802    // Step 18. Return response.
1803    response
1804}
1805
1806/// If the cache is not ready to construct a response, wait.
1807///
1808/// The cache is not ready if a previous fetch checked the cache, found nothing,
1809/// and moved on to a network fetch, and hasn't updated the cache yet with a pending resource.
1810///
1811/// Note that this is a different workflow from the one involving `wait_for_cached_response`.
1812/// That one happens when a fetch gets a cache hit, and the resource is pending completion from the network.
1813#[servo_tracing::instrument(skip_all)]
1814async fn block_for_cache_ready<'a>(
1815    context: &'a FetchContext,
1816    http_request: &mut Request,
1817    done_chan: &mut DoneChannel,
1818    revalidating_flag: &mut bool,
1819    response: &mut Option<Response>,
1820) -> CachedResourcesOrGuard<'a> {
1821    let entry_key = CacheKey::new(http_request);
1822    let guard_result = context.state.http_cache.get_or_guard(entry_key).await;
1823
1824    match guard_result {
1825        CachedResourcesOrGuard::Guard(_) => {
1826            *done_chan = None;
1827        },
1828        CachedResourcesOrGuard::Value(ref cached_resources) => {
1829            // TODO(#33616): Step 8.23 Set httpCache to the result of determining the
1830            // HTTP cache partition, given httpRequest.
1831            // Step 8.25.1 Set storedResponse to the result of selecting a response from the httpCache,
1832            //              possibly needing validation, as per the "Constructing Responses from Caches"
1833            //              chapter of HTTP Caching, if any.
1834            let stored_response = construct_response(http_request, done_chan, cached_resources);
1835            // Step 8.25.2 If storedResponse is non-null, then:
1836            if let Some(response_from_cache) = stored_response {
1837                let response_headers = response_from_cache.response.headers.clone();
1838                let validation_status = response_from_cache.validation_status;
1839                let revalidation_guard = response_from_cache.revalidation_guard.clone();
1840
1841                // Substep 1, 2, 3, 4
1842                let (cached_response, needs_synchronous_revalidation) =
1843                    match (http_request.cache_mode, &http_request.mode) {
1844                        (CacheMode::ForceCache, _) => (Some(response_from_cache.response), false),
1845                        (CacheMode::OnlyIfCached, &RequestMode::SameOrigin) => {
1846                            (Some(response_from_cache.response), false)
1847                        },
1848                        (CacheMode::OnlyIfCached, _) |
1849                        (CacheMode::NoStore, _) |
1850                        (CacheMode::Reload, _) => (None, false),
1851                        (_, _) => (
1852                            Some(response_from_cache.response),
1853                            validation_status ==
1854                                (ValidationStatus::Stale {
1855                                    revalidate_in_background: false,
1856                                }),
1857                        ),
1858                    };
1859
1860                if needs_synchronous_revalidation {
1861                    *revalidating_flag = true;
1862                    // Substep 5
1863                    if let Some(http_date) = response_headers.typed_get::<LastModified>() {
1864                        let http_date: SystemTime = http_date.into();
1865                        http_request
1866                            .headers
1867                            .typed_insert(IfModifiedSince::from(http_date));
1868                    }
1869                    if let Some(entity_tag) = response_headers.get(header::ETAG) {
1870                        http_request
1871                            .headers
1872                            .insert(header::IF_NONE_MATCH, entity_tag.clone());
1873                    }
1874                } else {
1875                    // Substep 6
1876                    // If it's a stale-while-revalidate response, also refresh it in the background.
1877                    let revalidate_in_background = validation_status ==
1878                        (ValidationStatus::Stale {
1879                            revalidate_in_background: true,
1880                        });
1881                    if revalidate_in_background && cached_response.is_some() {
1882                        spawn_stale_while_revalidate(context, http_request, revalidation_guard);
1883                    }
1884                    *response = cached_response;
1885                    if let Some(response) = response {
1886                        response.cache_state = CacheState::Local;
1887                    }
1888                }
1889                if response.is_none() {
1890                    // Ensure the done chan is not set if we're not using the cached response,
1891                    // as the cache might have set it to Some if it constructed a pending response.
1892                    *done_chan = None;
1893                }
1894            }
1895        },
1896    }
1897    guard_result
1898}
1899
1900/// The cached (stale) response has already been returned to the caller; here we
1901/// fire off an independent fetch whose only purpose is to refresh the stored response.
1902fn spawn_stale_while_revalidate(
1903    context: &FetchContext,
1904    http_request: &Request,
1905    revalidation_guard: StdArc<AtomicBool>,
1906) {
1907    // Only proceed if we are the one who flips the guard from `false` to `true`.
1908    if revalidation_guard
1909        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1910        .is_err()
1911    {
1912        return;
1913    }
1914
1915    // By setting `CacheMode::NoCache` to the cloned request,
1916    // we ensure that the background revalidation fetch will always go to the network, and preventing an inifinite loop.
1917    let mut revalidation_request = http_request.clone();
1918    revalidation_request.cache_mode = CacheMode::NoCache;
1919
1920    // A background revalidation must not itself spin up service workers
1921    revalidation_request.service_workers_mode = ServiceWorkersMode::None;
1922
1923    let context = context.clone();
1924    debug!(
1925        "spawning stale-while-revalidate background revalidation for {:?}",
1926        revalidation_request.current_url()
1927    );
1928    spawn_task(async move {
1929        let mut target = DiscardFetch;
1930
1931        let _ = fetch(revalidation_request, &mut target, &context).await;
1932        revalidation_guard.store(false, Ordering::Release);
1933    });
1934}
1935
1936/// Wait for a cached response from channel.
1937/// Happens when a fetch gets a cache hit, and the resource is pending completion from the network.
1938async fn wait_for_inflight_requests(done_chan: &mut DoneChannel, response: &mut Option<Response>) {
1939    if let Some(ref mut ch) = *done_chan {
1940        // The cache constructed a response with a body of ResponseBody::Receiving.
1941        // We wait for the response in the cache to "finish",
1942        // with a body of either Done or Cancelled.
1943        assert!(response.is_some());
1944
1945        loop {
1946            match ch.1.recv().await {
1947                Some(Data::ContentLength(_)) | Some(Data::Payload(_)) | Some(Data::Error(_)) => {},
1948                Some(Data::Done) => break, // Return the full response as if it was initially cached as such.
1949                Some(Data::Cancelled) => {
1950                    // The response was cancelled while the fetch was ongoing.
1951                    break;
1952                },
1953                None => panic!("HTTP cache should always send Done or Cancelled"),
1954            }
1955        }
1956    }
1957    // Set done_chan back to None, it's cache-related usefulness ends here.
1958    *done_chan = None;
1959}
1960
1961/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
1962///
1963/// This is obtained from [cross_origin_resource_policy_check]
1964#[derive(PartialEq)]
1965enum CrossOriginResourcePolicy {
1966    Allowed,
1967    Blocked,
1968}
1969
1970enum ForNavigation {
1971    #[expect(dead_code)]
1972    Yes,
1973    No,
1974}
1975
1976/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
1977fn cross_origin_resource_policy_check(
1978    origin: &Origin,
1979    request_client: &RequestClient,
1980    response: &Response,
1981    for_navigation: ForNavigation,
1982) -> CrossOriginResourcePolicy {
1983    // Step 1. Set forNavigation to false if it is not given.
1984    //
1985    // That's the default value of the enum
1986
1987    // Step 2. Let embedderPolicy be settingsObject’s policy container’s embedder policy.
1988    let embedder_policy = &request_client.policy_container.embedder_policy;
1989
1990    // Step 3. If the cross-origin resource policy internal check with origin, "unsafe-none",
1991    // response, and forNavigation returns blocked, then return blocked.
1992    if cross_origin_resource_policy_internal_check(
1993        origin,
1994        EmbedderPolicyValue::UnsafeNone,
1995        response,
1996        &for_navigation,
1997    ) == CrossOriginResourcePolicy::Blocked
1998    {
1999        return CrossOriginResourcePolicy::Blocked;
2000    }
2001
2002    // TODO Step 4. If the cross-origin resource policy internal check with origin,
2003    // embedderPolicy’s report only value, response, and forNavigation returns blocked, then queue
2004    // a cross-origin embedder policy CORP violation report with response, settingsObject,
2005    // destination, and true.
2006
2007    // Step 5. If the cross-origin resource policy internal check with origin, embedderPolicy’s
2008    // value, response, and forNavigation returns allowed, then return allowed.
2009    if cross_origin_resource_policy_internal_check(
2010        origin,
2011        embedder_policy.value,
2012        response,
2013        &for_navigation,
2014    ) == CrossOriginResourcePolicy::Allowed
2015    {
2016        return CrossOriginResourcePolicy::Allowed;
2017    }
2018
2019    // TODO Step 6. Queue a cross-origin embedder policy CORP violation report with response,
2020    // settingsObject, destination, and false.
2021
2022    // Step 7. Return blocked.
2023    CrossOriginResourcePolicy::Blocked
2024}
2025
2026/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check>
2027fn cross_origin_resource_policy_internal_check(
2028    origin: &Origin,
2029    embedder_policy_value: EmbedderPolicyValue,
2030    response: &Response,
2031    for_navigation: &ForNavigation,
2032) -> CrossOriginResourcePolicy {
2033    // Step 1. If forNavigation is true and embedderPolicyValue is "unsafe-none", then return allowed.
2034    if let ForNavigation::Yes = for_navigation &&
2035        let EmbedderPolicyValue::UnsafeNone = embedder_policy_value
2036    {
2037        return CrossOriginResourcePolicy::Allowed;
2038    }
2039
2040    // Step 2. Let policy be the result of getting `Cross-Origin-Resource-Policy` from response’s header list.
2041    let policy = response
2042        .headers
2043        .get(HeaderName::from_static("cross-origin-resource-policy"))
2044        .and_then(|h| h.to_str().ok());
2045
2046    // Step 3. If policy is neither `same-origin`, `same-site`, nor `cross-origin`, then set policy to null.
2047    let policy = policy
2048        .filter(|&s| s == "same-origin" || s == "same-site" || s == "cross-origin")
2049        // Step 4. If policy is null, then switch on embedderPolicyValue:
2050        .or(match embedder_policy_value {
2051            // Do nothing.
2052            EmbedderPolicyValue::UnsafeNone => None,
2053            // Set policy to `same-origin`.
2054            EmbedderPolicyValue::RequireCorp => Some("same-origin"),
2055        });
2056
2057    // Step 5. Switch on policy:
2058    match policy {
2059        Some("same-origin") => {
2060            // If origin is same origin with response’s URL’s origin, then return allowed.
2061            if let Origin::Origin(request_origin) = origin &&
2062                response
2063                    .url()
2064                    .is_some_and(|url| request_origin == &url.origin())
2065            {
2066                return CrossOriginResourcePolicy::Allowed;
2067            }
2068
2069            // Otherwise, return blocked.
2070            CrossOriginResourcePolicy::Blocked
2071        },
2072        Some("same-site") => {
2073            if let Some(response_url) = response.url() {
2074                // If all of the following are true
2075                // origin is schemelessly same site with response’s URL’s origin
2076                // origin’s scheme is "https" or response’s URL’s scheme is not "https"
2077                if let Origin::Origin(request_origin) = origin &&
2078                    is_schemelessy_same_site(request_origin, &response_url.origin()) &&
2079                    (request_origin.scheme() == Some("https") ||
2080                        response_url.scheme() != "https")
2081                {
2082                    return CrossOriginResourcePolicy::Allowed;
2083                }
2084            }
2085            // Otherwise, return blocked.
2086            CrossOriginResourcePolicy::Blocked
2087        },
2088        // null / 'cross-origin'
2089        // Return allowed.
2090        _ => CrossOriginResourcePolicy::Allowed,
2091    }
2092}
2093
2094// Convenience struct that implements Done, for setting responseEnd on function return
2095struct ResponseEndTimer(Option<ResourceFetchTimingContainer>);
2096
2097impl ResponseEndTimer {
2098    fn neuter(&mut self) {
2099        self.0 = None;
2100    }
2101}
2102
2103impl Drop for ResponseEndTimer {
2104    fn drop(&mut self) {
2105        let ResponseEndTimer(resource_fetch_timing_opt) = self;
2106
2107        resource_fetch_timing_opt.as_ref().map_or((), |t| {
2108            t.set_attribute(ResourceAttribute::ResponseEnd);
2109        })
2110    }
2111}
2112
2113/// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch)
2114#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
2115async fn http_network_fetch(
2116    fetch_params: &mut FetchParams,
2117    credentials_flag: bool,
2118    done_chan: &mut DoneChannel,
2119    context: &FetchContext,
2120) -> Response {
2121    let mut response_end_timer = ResponseEndTimer(Some(context.timing.clone()));
2122
2123    // Step 1: Let request be fetchParams’s request.
2124    let request = &mut fetch_params.request;
2125
2126    // TODO Step 2. If request’s client is offline, then return a network error.
2127
2128    // TODO Step 3. Let response be null.
2129
2130    // TODO Step 4. Let timingInfo be fetchParams’s timing info.
2131
2132    // TODO Step 5. Let networkPartitionKey be the result of determining the network partition key
2133    // given request.
2134
2135    let url = request.current_url();
2136    let request_id = request.id.0.to_string();
2137    if log_enabled!(log::Level::Info) {
2138        info!("{:?} request for {}", request.method, url);
2139        for header in request.headers.iter() {
2140            debug!(" - {:?}", header);
2141        }
2142    }
2143
2144    // XHR uses the default destination; other kinds of fetches (which haven't been implemented yet)
2145    // do not. Once we support other kinds of fetches we'll need to be more fine grained here
2146    // since things like image fetches are classified differently by devtools
2147    let is_xhr = request.destination == Destination::None;
2148
2149    // The receiver will receive true if there has been an error streaming the request body.
2150    let (fetch_terminated_sender, mut fetch_terminated_receiver) = unbounded_channel();
2151
2152    let body = request.body.as_ref().map(|body| body.clone_stream());
2153
2154    if body.is_none() {
2155        // There cannot be an error streaming a non-existent body.
2156        // However in such a case the channel will remain unused
2157        // and drop inside `obtain_response`.
2158        // Send the confirmation now, ensuring the receiver will not dis-connect first.
2159        let _ = fetch_terminated_sender.send(false);
2160    }
2161
2162    let browsing_context_id = request.target_webview_id.map(Into::into);
2163
2164    // Step 6. Let newConnection be "yes" if forceNewConnection is true; otherwise "no".
2165
2166    // Step 7. Switch on request’s mode:
2167    let (response_stream, msg) = match &request.mode {
2168        // Let connection be the result of obtaining a WebSocket connection, given request’s current URL.
2169        RequestMode::WebSocket {
2170            protocols,
2171            original_url: _,
2172        } => {
2173            // https://fetch.spec.whatwg.org/#websocket-opening-handshake
2174
2175            let (resource_event_sender, dom_action_receiver) = {
2176                let mut websocket_chan = context.websocket_chan.as_ref().unwrap().lock();
2177                (
2178                    websocket_chan.sender.clone(),
2179                    websocket_chan.receiver.take().unwrap(),
2180                )
2181            };
2182
2183            let mut tls_config = create_tls_config(
2184                context.ca_certificates.clone(),
2185                context.ignore_certificate_errors,
2186                context.state.override_manager.clone(),
2187            );
2188            tls_config.alpn_protocols = vec!["http/1.1".to_string().into()];
2189
2190            let response = match start_websocket(
2191                context.state.clone(),
2192                resource_event_sender,
2193                protocols,
2194                request,
2195                tls_config,
2196                dom_action_receiver,
2197            )
2198            .await
2199            {
2200                Ok(response) => response,
2201                Err(error) => {
2202                    return Response::network_error(NetworkError::WebsocketConnectionFailure(
2203                        format!("{error:?}"),
2204                    ));
2205                },
2206            };
2207
2208            let response = response.map(|r| match r {
2209                Some(body) => Full::from(body).map_err(|_| unreachable!()).boxed(),
2210                None => http_body_util::Empty::new()
2211                    .map_err(|_| unreachable!())
2212                    .boxed(),
2213            });
2214            (Decoder::detect(response, url.is_secure_scheme()), None)
2215        },
2216        // Let connection be the result of obtaining a connection, given networkPartitionKey,
2217        // request’s current URL, includeCredentials, and newConnection.
2218        _ => {
2219            let response_future = obtain_response(
2220                &context.state.client,
2221                &url,
2222                &request.method,
2223                &mut request.headers,
2224                body,
2225                request
2226                    .body
2227                    .as_ref()
2228                    .is_some_and(|body| body.source_is_null()),
2229                &request.pipeline_id,
2230                Some(&request_id),
2231                request.destination,
2232                is_xhr,
2233                context,
2234                fetch_terminated_sender,
2235                browsing_context_id,
2236            );
2237
2238            // This will only get the headers, the body is read later
2239            let (response_stream, msg) = match response_future.await {
2240                Ok(wrapped_response) => wrapped_response,
2241                Err(error) => return Response::network_error(error),
2242            };
2243            (response_stream, msg)
2244        },
2245    };
2246
2247    if log_enabled!(log::Level::Info) {
2248        debug!("{:?} response for {}", response_stream.version(), url);
2249        for header in response_stream.headers().iter() {
2250            debug!(" - {:?}", header);
2251        }
2252    }
2253
2254    // Check if there was an error while streaming the request body.
2255    //
2256    match fetch_terminated_receiver.recv().await {
2257        Some(true) => return Response::network_error(NetworkError::ConnectionFailure),
2258        Some(false) => {},
2259        _ => warn!("Failed to receive confirmation request was streamed without error."),
2260    }
2261
2262    let timing = context.timing.inner().clone();
2263    let mut response = Response::new(url.clone(), timing);
2264
2265    if let Some(handshake_info) = response_stream.extensions().get::<TlsHandshakeInfo>() {
2266        let mut hsts_enabled = url
2267            .host_str()
2268            .is_some_and(|host| context.state.hsts_list.read().is_host_secure(host));
2269
2270        if url.scheme() == "https" &&
2271            let Some(strict_transport_security) = response_stream
2272                .headers()
2273                .typed_get::<StrictTransportSecurity>()
2274        {
2275            // max-age > 0 enables HSTS, max-age = 0 disables it (RFC 6797 Section 6.1.1)
2276            hsts_enabled = strict_transport_security.max_age().as_secs() > 0;
2277        }
2278        response.tls_security_info = Some(build_tls_security_info(handshake_info, hsts_enabled));
2279    }
2280
2281    let status_text = response_stream
2282        .extensions()
2283        .get::<ReasonPhrase>()
2284        .map(ReasonPhrase::as_bytes)
2285        .or_else(|| {
2286            response_stream
2287                .status()
2288                .canonical_reason()
2289                .map(str::as_bytes)
2290        })
2291        .map(Vec::from)
2292        .unwrap_or_default();
2293    response.status = HttpStatus::new(response_stream.status(), status_text);
2294
2295    info!(
2296        "got {:?} response for {:?}",
2297        response_stream.status(),
2298        request.url()
2299    );
2300    response.headers = response_stream.headers().clone();
2301    response.referrer = request.referrer.to_url().cloned();
2302    response.referrer_policy = request.referrer_policy;
2303
2304    let response_body = response.body.clone();
2305
2306    // We're about to spawn a future to be waited on here
2307    let (done_sender, done_receiver) = unbounded_channel();
2308    *done_chan = Some((done_sender.clone(), done_receiver));
2309
2310    let devtools_sender = context.devtools_chan.clone();
2311    let cancellation_listener = context.cancellation_listener.clone();
2312    if cancellation_listener.cancelled() {
2313        return Response::network_error(NetworkError::LoadCancelled);
2314    }
2315
2316    if let Some(ref sender) = devtools_sender &&
2317        let Some(m) = msg
2318    {
2319        send_request_to_devtools(m, sender);
2320    }
2321
2322    let done_sender2 = done_sender.clone();
2323    let done_sender3 = done_sender.clone();
2324    let timing_ptr2 = context.timing.clone();
2325    let timing_ptr3 = context.timing.clone();
2326    let devtools_request = request.clone();
2327    let url1 = devtools_request.url();
2328    let url2 = url1.clone();
2329
2330    let status = response.status.clone();
2331    let headers = response.headers.clone();
2332    let devtools_chan = context.devtools_chan.clone();
2333
2334    let prealloc_size: usize = if let Some(possible_length) = response_stream
2335        .headers()
2336        .get(http::header::CONTENT_LENGTH)
2337        .and_then(|header_value| header_value.to_str().ok())
2338        .and_then(|s| s.parse::<usize>().ok())
2339    {
2340        // For compressed content, we pre-allocate a multiple of the
2341        // compressed size, assuming typical content will be highly compressed.
2342        let multiplier: usize = if response_stream.body().is_encoded() {
2343            5
2344        } else {
2345            1
2346        };
2347        possible_length.saturating_mul(multiplier)
2348    } else {
2349        // We don't know the length, so we fallback to something to still
2350        // avoid some reallocs.
2351        4096
2352    }
2353    .min(pref!(network_max_content_length) as usize);
2354    let _ = done_sender.send(Data::ContentLength(prealloc_size));
2355    *response_body.lock() = ResponseBody::Receiving(Vec::with_capacity(prealloc_size));
2356    let response_body2 = response_body.clone();
2357
2358    spawn_task(
2359        response_stream
2360            .into_body()
2361            .try_fold(response_body, move |response_body_accumulator, chunk| {
2362                if cancellation_listener.cancelled() {
2363                    *response_body_accumulator.lock() = ResponseBody::Done(vec![]);
2364                    let _ = done_sender.send(Data::Cancelled);
2365                    return future::ready(Err(std::io::Error::new(
2366                        std::io::ErrorKind::Interrupted,
2367                        "Fetch aborted",
2368                    )));
2369                }
2370                if let ResponseBody::Receiving(ref mut body) = *response_body_accumulator.lock() {
2371                    body.extend_from_slice(&chunk);
2372                    let _ = done_sender.send(Data::Payload(chunk));
2373                }
2374                future::ready(Ok(response_body_accumulator))
2375            })
2376            .and_then(move |complete_response_body| {
2377                debug!("successfully finished response for {:?}", url1);
2378                let mut body = complete_response_body.lock();
2379                let mut completed_body = match *body {
2380                    ResponseBody::Receiving(ref mut body) => std::mem::take(body),
2381                    _ => vec![],
2382                };
2383                // This allocation may be retained by the http-cache.
2384                completed_body.shrink_to_fit();
2385                // If devtools is disabled avoid cloning, since the result would
2386                // be unused anyway.
2387                let devtools_response_body =
2388                    devtools_chan.is_some().then(|| completed_body.clone());
2389                *body = ResponseBody::Done(completed_body);
2390                send_response_values_to_devtools(
2391                    Some(headers),
2392                    status,
2393                    devtools_response_body,
2394                    CacheState::None,
2395                    &devtools_request,
2396                    devtools_chan,
2397                );
2398                timing_ptr2.set_attribute(ResourceAttribute::ResponseEnd);
2399                let _ = done_sender2.send(Data::Done);
2400                future::ready(Ok(()))
2401            })
2402            .map_err(move |error| {
2403                if let std::io::ErrorKind::InvalidData = error.kind() {
2404                    debug!("Content decompression error for {:?}", url2);
2405                    let _ = done_sender3.send(Data::Error(NetworkError::DecompressionError));
2406                    let mut body = response_body2.lock();
2407
2408                    *body = ResponseBody::Done(vec![]);
2409                }
2410                debug!("finished response for {:?}", url2);
2411                let mut body = response_body2.lock();
2412                let completed_body = match *body {
2413                    ResponseBody::Receiving(ref mut body) => std::mem::take(body),
2414                    _ => vec![],
2415                };
2416                *body = ResponseBody::Done(completed_body);
2417                timing_ptr3.set_attribute(ResourceAttribute::ResponseEnd);
2418                let _ = done_sender3.send(Data::Done);
2419            }),
2420    );
2421
2422    // TODO these substeps aren't possible yet
2423    // Substep 1
2424
2425    // Substep 2
2426
2427    // TODO Read request
2428
2429    // Step 6-11
2430    // (needs stream bodies)
2431
2432    // Step 13
2433    // TODO this step isn't possible yet (CSP)
2434
2435    // Step 14, update the cached response, done via the shared response body.
2436
2437    // TODO this step isn't possible yet
2438    // Step 15
2439    if credentials_flag {
2440        set_cookies_from_headers(&url, &response.headers, &context.state.cookie_jar);
2441    }
2442    context
2443        .state
2444        .hsts_list
2445        .write()
2446        .update_hsts_list_from_response(&url, &response.headers);
2447
2448    // TODO these steps
2449    // Step 16
2450    // Substep 1
2451    // Substep 2
2452    // Sub-substep 1
2453    // Sub-substep 2
2454    // Sub-substep 3
2455    // Sub-substep 4
2456    // Substep 3
2457
2458    // Step 16
2459
2460    // Ensure we don't override "responseEnd" on successful return of this function
2461    response_end_timer.neuter();
2462    response
2463}
2464
2465/// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch)
2466async fn cors_preflight_fetch(
2467    request: &Request,
2468    cache: &mut CorsCache,
2469    context: &FetchContext,
2470) -> Response {
2471    // Step 1. Let preflight be a new request whose method is `OPTIONS`, URL list is a clone
2472    // of request’s URL list, initiator is request’s initiator, destination is request’s destination,
2473    // origin is request’s origin, referrer is request’s referrer, referrer policy is request’s
2474    // referrer policy, mode is "cors", and response tainting is "cors".
2475    let mut preflight = RequestBuilder::new(
2476        request.target_webview_id,
2477        request.current_url_with_blob_claim(),
2478        request.referrer.clone(),
2479    )
2480    .method(Method::OPTIONS)
2481    .origin(match &request.origin {
2482        Origin::Client => {
2483            unreachable!("We shouldn't get Client origin in cors_preflight_fetch.")
2484        },
2485        Origin::Origin(origin) => origin.clone(),
2486    })
2487    .pipeline_id(request.pipeline_id)
2488    .initiator(request.initiator)
2489    .destination(request.destination)
2490    .referrer_policy(request.referrer_policy)
2491    .mode(RequestMode::CorsMode)
2492    .response_tainting(ResponseTainting::CorsTainting)
2493    .policy_container(match &request.policy_container {
2494        RequestPolicyContainer::Client => {
2495            unreachable!("We should have a policy container for request in cors_preflight_fetch")
2496        },
2497        RequestPolicyContainer::PolicyContainer(policy_container) => policy_container.clone(),
2498    })
2499    .url_list(
2500        request
2501            .url_list
2502            .iter()
2503            .map(|claimed_url| claimed_url.url())
2504            .collect(),
2505    )
2506    .build();
2507
2508    // Step 2. Append (`Accept`, `*/*`) to preflight’s header list.
2509    preflight
2510        .headers
2511        .insert(ACCEPT, HeaderValue::from_static("*/*"));
2512
2513    // Step 3. Append (`Access-Control-Request-Method`, request’s method) to preflight’s header list.
2514    preflight
2515        .headers
2516        .typed_insert::<AccessControlRequestMethod>(AccessControlRequestMethod::from(
2517            request.method.clone(),
2518        ));
2519
2520    // Step 4. Let headers be the CORS-unsafe request-header names with request’s header list.
2521    let headers = get_cors_unsafe_header_names(&request.headers);
2522
2523    // Step 5 If headers is not empty, then:
2524    if !headers.is_empty() {
2525        // 5.1 Let value be the items in headers separated from each other by `,`
2526        // TODO(36451): replace this with typed_insert when headers fixes headers#207
2527        preflight.headers.insert(
2528            ACCESS_CONTROL_REQUEST_HEADERS,
2529            HeaderValue::from_bytes(itertools::join(headers.iter(), ",").as_bytes())
2530                .unwrap_or(HeaderValue::from_static("")),
2531        );
2532    }
2533
2534    // Step 6. Let response be the result of running HTTP-network-or-cache fetch given a
2535    // new fetch params whose request is preflight.
2536    let mut fetch_params = FetchParams::new(preflight);
2537    let response =
2538        http_network_or_cache_fetch(&mut fetch_params, false, false, &mut None, context).await;
2539
2540    // Step 7. If a CORS check for request and response returns success and response’s status is an ok status, then:
2541    if cors_check(request, &response).is_ok() && response.status.code().is_success() {
2542        // Step 7.1 Let methods be the result of extracting header list values given
2543        // `Access-Control-Allow-Methods` and response’s header list.
2544        let mut methods = if response
2545            .headers
2546            .contains_key(header::ACCESS_CONTROL_ALLOW_METHODS)
2547        {
2548            match response.headers.typed_get::<AccessControlAllowMethods>() {
2549                Some(methods) => methods.iter().collect(),
2550                // Step 7.3 If either methods or headerNames is failure, return a network error.
2551                None => {
2552                    return Response::network_error(NetworkError::CorsAllowMethods);
2553                },
2554            }
2555        } else {
2556            vec![]
2557        };
2558
2559        // Step 7.2 Let headerNames be the result of extracting header list values given
2560        // `Access-Control-Allow-Headers` and response’s header list.
2561        let header_names = if response
2562            .headers
2563            .contains_key(header::ACCESS_CONTROL_ALLOW_HEADERS)
2564        {
2565            match response.headers.typed_get::<AccessControlAllowHeaders>() {
2566                Some(names) => names.iter().collect(),
2567                // Step 7.3 If either methods or headerNames is failure, return a network error.
2568                None => {
2569                    return Response::network_error(NetworkError::CorsAllowHeaders);
2570                },
2571            }
2572        } else {
2573            vec![]
2574        };
2575
2576        debug!(
2577            "CORS check: Allowed methods: {:?}, current method: {:?}",
2578            methods, request.method
2579        );
2580
2581        // Step 7.4 If methods is null and request’s use-CORS-preflight flag is set,
2582        // then set methods to a new list containing request’s method.
2583        if methods.is_empty() && request.use_cors_preflight {
2584            methods = vec![request.method.clone()];
2585        }
2586
2587        // Step 7.5 If request’s method is not in methods, request’s method is not a CORS-safelisted method,
2588        // and request’s credentials mode is "include" or methods does not contain `*`, then return a network error.
2589        if methods
2590            .iter()
2591            .all(|method| *method.as_str() != *request.method.as_ref()) &&
2592            !is_cors_safelisted_method(&request.method) &&
2593            (request.credentials_mode == CredentialsMode::Include ||
2594                methods.iter().all(|method| method.as_ref() != "*"))
2595        {
2596            return Response::network_error(NetworkError::CorsMethod);
2597        }
2598
2599        debug!(
2600            "CORS check: Allowed headers: {:?}, current headers: {:?}",
2601            header_names, request.headers
2602        );
2603
2604        // Step 7.6 If one of request’s header list’s names is a CORS non-wildcard request-header name
2605        // and is not a byte-case-insensitive match for an item in headerNames, then return a network error.
2606        //
2607        // Note: This check deviates from the spec. Other browsers (Chrome, Firefox, Safari) all treat a
2608        // `*` in headerNames as covering CORS non-wildcard request-header names.
2609        let header_names_set: HashSet<&HeaderName> = HashSet::from_iter(header_names.iter());
2610        if request.headers.iter().any(|(name, _)| {
2611            is_cors_non_wildcard_request_header_name(name) &&
2612                !header_names_set.contains(name) &&
2613                !header_names_set.contains(&HeaderName::from_static("*"))
2614        }) {
2615            return Response::network_error(NetworkError::CorsAuthorization);
2616        }
2617
2618        // Step 7.7 For each unsafeName of the CORS-unsafe request-header names with request’s header list,
2619        // if unsafeName is not a byte-case-insensitive match for an item in headerNames and request’s credentials
2620        // mode is "include" or headerNames does not contain `*`, return a network error.
2621        let unsafe_names = get_cors_unsafe_header_names(&request.headers);
2622        for unsafe_name in unsafe_names.iter() {
2623            if !header_names_set.contains(unsafe_name) &&
2624                (request.credentials_mode == CredentialsMode::Include ||
2625                    !header_names_set.contains(&HeaderName::from_static("*")))
2626            {
2627                return Response::network_error(NetworkError::CorsHeaders);
2628            }
2629        }
2630
2631        // Step 7.8 Let max-age be the result of extracting header list values given
2632        // `Access-Control-Max-Age` and response’s header list.
2633        let max_age: Option<Duration> = response
2634            .headers
2635            .typed_get::<AccessControlMaxAge>()
2636            .map(|acma| acma.into());
2637
2638        // Step 7.9 If max-age is failure or null, then set max-age to 5.
2639        let max_age = max_age.unwrap_or(Duration::from_secs(5));
2640
2641        // Step 7.10 If max-age is greater than an imposed limit on max-age, then set max-age to the imposed limit.
2642        // TODO: Need to define what an imposed limit on max-age is
2643
2644        // Step 7.11 If the user agent does not provide for a cache, then return response.
2645        // NOTE: This can be ignored, we do have a CORS cache
2646
2647        // Step 7.12 For each method in methods for which there is a method cache entry match using request,
2648        // set matching entry’s max-age to max-age.
2649        // Step 7.13 For each method in methods for which there is no method cache entry match using request,
2650        // create a new cache entry with request, max-age, method, and null.
2651        for method in &methods {
2652            cache.match_method_and_update(request, method.clone(), max_age);
2653        }
2654
2655        // Step 7.14 For each headerName in headerNames for which there is a header-name cache entry match using request,
2656        // set matching entry’s max-age to max-age.
2657        // Step 7.15 For each headerName in headerNames for which there is no header-name cache entry match using request,
2658        // create a new cache entry with request, max-age, null, and headerName.
2659        for header_name in &header_names {
2660            cache.match_header_and_update(request, header_name, max_age);
2661        }
2662
2663        // Step 7.16 Return response.
2664        return response;
2665    }
2666
2667    // Step 8 Return a network error.
2668    Response::network_error(NetworkError::CorsGeneral)
2669}
2670
2671/// [CORS check](https://fetch.spec.whatwg.org#concept-cors-check)
2672fn cors_check(request: &Request, response: &Response) -> Result<(), ()> {
2673    // Step 1. Let origin be the result of getting `Access-Control-Allow-Origin` from response’s header list.
2674    let Some(origins) =
2675        get_value_from_header_list(ACCESS_CONTROL_ALLOW_ORIGIN.as_str(), &response.headers)
2676    else {
2677        // Step 2. If origin is null, then return failure.
2678        return Err(());
2679    };
2680    let origin = origins.into_iter().map(char::from).collect::<String>();
2681
2682    // Step 3. If request’s credentials mode is not "include" and origin is `*`, then return success.
2683    if request.credentials_mode != CredentialsMode::Include && origin == "*" {
2684        return Ok(());
2685    }
2686
2687    // Step 4. If the result of byte-serializing a request origin with request is not origin, then return failure.
2688    if serialize_request_origin(request).to_string() != origin {
2689        return Err(());
2690    }
2691
2692    // Step 5. If request’s credentials mode is not "include", then return success.
2693    if request.credentials_mode != CredentialsMode::Include {
2694        return Ok(());
2695    }
2696
2697    // Step 6. Let credentials be the result of getting `Access-Control-Allow-Credentials` from response’s header list.
2698    let credentials = response
2699        .headers
2700        .typed_get::<AccessControlAllowCredentials>();
2701
2702    // Step 7. If credentials is `true`, then return success.
2703    if credentials.is_some() {
2704        return Ok(());
2705    }
2706
2707    // Step 8. Return failure.
2708    Err(())
2709}
2710
2711fn has_credentials(url: &ServoUrl) -> bool {
2712    !url.username().is_empty() || url.password().is_some()
2713}
2714
2715fn is_no_store_cache(headers: &HeaderMap) -> bool {
2716    headers.contains_key(header::IF_MODIFIED_SINCE) |
2717        headers.contains_key(header::IF_NONE_MATCH) |
2718        headers.contains_key(header::IF_UNMODIFIED_SINCE) |
2719        headers.contains_key(header::IF_MATCH) |
2720        headers.contains_key(header::IF_RANGE)
2721}
2722
2723/// <https://fetch.spec.whatwg.org/#redirect-status>
2724fn is_redirect_status(status: StatusCode) -> bool {
2725    matches!(
2726        status,
2727        StatusCode::MOVED_PERMANENTLY |
2728            StatusCode::FOUND |
2729            StatusCode::SEE_OTHER |
2730            StatusCode::TEMPORARY_REDIRECT |
2731            StatusCode::PERMANENT_REDIRECT
2732    )
2733}
2734
2735/// <https://fetch.spec.whatwg.org/#serializing-a-request-origin>
2736fn serialize_request_origin(request: &Request) -> headers::Origin {
2737    // Step 1. Assert: request’s origin is not "client".
2738    let Origin::Origin(origin) = &request.origin else {
2739        panic!("origin cannot be \"client\" at this point in time");
2740    };
2741
2742    // Step 2. If request’s redirect-taint is not "same-origin", then return "null".
2743    if request.redirect_taint_for_request() != RedirectTaint::SameOrigin {
2744        return headers::Origin::NULL;
2745    }
2746
2747    // Step 3. Return request’s origin, serialized.
2748    serialize_origin(origin)
2749}
2750
2751/// Step 3 of <https://fetch.spec.whatwg.org/#serializing-a-request-origin>.
2752pub fn serialize_origin(origin: &ImmutableOrigin) -> headers::Origin {
2753    match origin {
2754        ImmutableOrigin::Opaque(_) => headers::Origin::NULL,
2755        ImmutableOrigin::Tuple(scheme, host, port) => {
2756            // Note: This must be kept in sync with `Origin::ascii_serialization()`, which does not
2757            // use the port number when a default port is used.
2758            let port = match (scheme.as_ref(), port) {
2759                ("http" | "ws", 80) | ("https" | "wss", 443) | ("ftp", 21) => None,
2760                _ => Some(*port),
2761            };
2762
2763            // TODO: Ensure that hyper/servo don't disagree about valid origin headers
2764            headers::Origin::try_from_parts(scheme, &host.to_string(), port)
2765                .unwrap_or(headers::Origin::NULL)
2766        },
2767    }
2768}
2769
2770/// <https://fetch.spec.whatwg.org/#append-a-request-origin-header>
2771#[expect(
2772    clippy::collapsible_match,
2773    reason = "The current way follows the spec more closely"
2774)]
2775fn append_a_request_origin_header(request: &mut Request) {
2776    // Step 1. Assert: request’s origin is not "client".
2777    let Origin::Origin(request_origin) = &request.origin else {
2778        panic!("origin cannot be \"client\" at this point in time");
2779    };
2780
2781    // Step 2. Let serializedOrigin be the result of byte-serializing a request origin with request.
2782    let mut serialized_origin = serialize_request_origin(request);
2783
2784    // Step 3. If request’s response tainting is "cors" or request’s mode is "websocket",
2785    //         then append (`Origin`, serializedOrigin) to request’s header list.
2786    if request.response_tainting == ResponseTainting::CorsTainting ||
2787        matches!(request.mode, RequestMode::WebSocket { .. })
2788    {
2789        request.headers.typed_insert(serialized_origin);
2790    }
2791    // Step 4. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
2792    else if !matches!(request.method, Method::GET | Method::HEAD) {
2793        // Step 4.1 If request’s mode is not "cors", then switch on request’s referrer policy:
2794        if request.mode != RequestMode::CorsMode {
2795            match request.referrer_policy {
2796                ReferrerPolicy::NoReferrer => {
2797                    // Set serializedOrigin to `null`.
2798                    serialized_origin = headers::Origin::NULL;
2799                },
2800                ReferrerPolicy::NoReferrerWhenDowngrade |
2801                ReferrerPolicy::StrictOrigin |
2802                ReferrerPolicy::StrictOriginWhenCrossOrigin => {
2803                    // If request’s origin is a tuple origin, its scheme is "https", and
2804                    // request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
2805                    if let ImmutableOrigin::Tuple(scheme, _, _) = &request_origin &&
2806                        scheme == "https" &&
2807                        request.current_url().scheme() != "https"
2808                    {
2809                        serialized_origin = headers::Origin::NULL;
2810                    }
2811                },
2812                ReferrerPolicy::SameOrigin => {
2813                    // If request’s origin is not same origin with request’s current URL’s origin,
2814                    // then set serializedOrigin to `null`.
2815                    if *request_origin != request.current_url().origin() {
2816                        serialized_origin = headers::Origin::NULL;
2817                    }
2818                },
2819                _ => {
2820                    // Otherwise, do nothing.
2821                },
2822            };
2823        }
2824
2825        // Step 4.2. Append (`Origin`, serializedOrigin) to request’s header list.
2826        request.headers.typed_insert(serialized_origin);
2827    }
2828}
2829
2830/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-append-the-fetch-metadata-headers-for-a-request>
2831fn append_the_fetch_metadata_headers(r: &mut Request) {
2832    // Step 1. If r’s current url is not an potentially trustworthy URL, return.
2833    if !r.current_url().is_potentially_trustworthy() {
2834        return;
2835    }
2836
2837    // Step 2. Set the Sec-Fetch-Dest header for r.
2838    set_the_sec_fetch_dest_header(r);
2839
2840    // Step 3. Set the Sec-Fetch-Mode header for r.
2841    set_the_sec_fetch_mode_header(r);
2842
2843    // Step 4. Set the Sec-Fetch-Site header for r.
2844    set_the_sec_fetch_site_header(r);
2845
2846    // Step 5. Set the Sec-Fetch-User header for r.
2847    set_the_sec_fetch_user_header(r);
2848}
2849
2850/// Steps 8.16 to 8.18 in [HTTP network or cache fetch](https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch)
2851fn append_cache_data_to_headers(http_request: &mut Request) {
2852    match http_request.cache_mode {
2853        // Step 8.16: If httpRequest’s cache mode is "default" and httpRequest’s header list
2854        // contains `If-Modified-Since`, `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or
2855        // `If-Range`, then set httpRequest’s cache mode to "no-store".
2856        CacheMode::Default if is_no_store_cache(&http_request.headers) => {
2857            http_request.cache_mode = CacheMode::NoStore;
2858        },
2859
2860        // Note that the following steps (8.17 and 8.18) are being considered for removal:
2861        // https://github.com/whatwg/fetch/issues/722#issuecomment-1420264615
2862
2863        // Step 8.17: If httpRequest’s cache mode is "no-cache", httpRequest’s prevent no-cache
2864        // cache-control header modification flag is unset, and httpRequest’s header list does not
2865        // contain `Cache-Control`, then append (`Cache-Control`, `max-age=0`) to httpRequest’s
2866        // header list.
2867        // TODO: Implement request's prevent no-cache cache-control header modification flag
2868        // https://fetch.spec.whatwg.org/#no-cache-prevent-cache-control
2869        CacheMode::NoCache if !http_request.headers.contains_key(header::CACHE_CONTROL) => {
2870            http_request
2871                .headers
2872                .typed_insert(CacheControl::new().with_max_age(Duration::from_secs(0)));
2873        },
2874
2875        // Step 8.18: If httpRequest’s cache mode is "no-store" or "reload", then:
2876        CacheMode::Reload | CacheMode::NoStore => {
2877            // Step 8.18.1: If httpRequest’s header list does not contain `Pragma`, then append
2878            // (`Pragma`, `no-cache`) to httpRequest’s header list.
2879            if !http_request.headers.contains_key(header::PRAGMA) {
2880                http_request.headers.typed_insert(Pragma::no_cache());
2881            }
2882
2883            // Step 8.18.2: If httpRequest’s header list does not contain `Cache-Control`, then
2884            // append (`Cache-Control`, `no-cache`) to httpRequest’s header list.
2885            if !http_request.headers.contains_key(header::CACHE_CONTROL) {
2886                http_request
2887                    .headers
2888                    .typed_insert(CacheControl::new().with_no_cache());
2889            }
2890        },
2891
2892        _ => {},
2893    }
2894}
2895
2896/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest>
2897fn set_the_sec_fetch_dest_header(r: &mut Request) {
2898    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2899    debug_assert!(r.current_url().is_potentially_trustworthy());
2900
2901    // Step 2. Let header be a Structured Header whose value is a token.
2902    // Step 3. If r’s destination is the empty string, set header’s value to the string "empty".
2903    // Otherwise, set header’s value to r’s destination.
2904    let header = r.destination;
2905
2906    // Step 4. Set a structured field value `Sec-Fetch-Dest`/header in r’s header list.
2907    r.headers.typed_insert(SecFetchDest(header));
2908}
2909
2910/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-mode>
2911fn set_the_sec_fetch_mode_header(r: &mut Request) {
2912    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2913    debug_assert!(r.current_url().is_potentially_trustworthy());
2914
2915    // Step 2. Let header be a Structured Header whose value is a token.
2916    // Step 3. Set header’s value to r’s mode.
2917    let header = &r.mode;
2918
2919    // Step 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
2920    r.headers.typed_insert(SecFetchMode::from(header));
2921}
2922
2923/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-site>
2924fn set_the_sec_fetch_site_header(r: &mut Request) {
2925    // The webappsec spec seems to have a similar issue as
2926    // https://github.com/whatwg/fetch/issues/1773
2927    let Origin::Origin(request_origin) = &r.origin else {
2928        panic!("request origin cannot be \"client\" at this point")
2929    };
2930
2931    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2932    debug_assert!(r.current_url().is_potentially_trustworthy());
2933
2934    // Step 2. Let header be a Structured Header whose value is a token.
2935    // Step 3. Set header’s value to same-origin.
2936    let mut header = SecFetchSite::SameOrigin;
2937
2938    // TODO: Step 3. If r is a navigation request that was explicitly caused by a
2939    // user’s interaction with the user agent, then set header’s value to none.
2940
2941    // Step 5. If header’s value is not none, then for each url in r’s url list:
2942    if header != SecFetchSite::None {
2943        for url in &r.url_list {
2944            // Step 5.1 If url is same origin with r’s origin, continue.
2945            if url.origin() == *request_origin {
2946                continue;
2947            }
2948
2949            // Step 5.2 Set header’s value to cross-site.
2950            header = SecFetchSite::CrossSite;
2951
2952            // Step 5.3 If r’s origin is not same site with url’s origin, then break.
2953            if !is_same_site(request_origin, &url.origin()) {
2954                break;
2955            }
2956
2957            // Step 5.4 Set header’s value to same-site.
2958            header = SecFetchSite::SameSite;
2959        }
2960    }
2961
2962    // Step 6. Set a structured field value `Sec-Fetch-Site`/header in r’s header list.
2963    r.headers.typed_insert(header);
2964}
2965
2966/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-user>
2967fn set_the_sec_fetch_user_header(r: &mut Request) {
2968    // Step 1. Assert: r’s current url is a potentially trustworthy URL.
2969    debug_assert!(r.current_url().is_potentially_trustworthy());
2970
2971    // Step 2. If r is not a navigation request, or if r’s user-activation is false, return.
2972    // TODO user activation
2973    if !r.is_navigation_request() {
2974        return;
2975    }
2976
2977    // Step 3. Let header be a Structured Header whose value is a token.
2978    // Step 4. Set header’s value to true.
2979    let header = SecFetchUser;
2980
2981    // Step 5. Set a structured field value `Sec-Fetch-User`/header in r’s header list.
2982    r.headers.typed_insert(header);
2983}
2984
2985/// <https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect>
2986fn set_requests_referrer_policy_on_redirect(request: &mut Request, response: &Response) {
2987    // Step 1: Let policy be the result of executing § 8.1 Parse a referrer policy from a
2988    // Referrer-Policy header on actualResponse.
2989    let referrer_policy: ReferrerPolicy = response
2990        .headers
2991        .typed_get::<headers::ReferrerPolicy>()
2992        .into();
2993
2994    // Step 2: If policy is not the empty string, then set request’s referrer policy to policy.
2995    if referrer_policy != ReferrerPolicy::EmptyString {
2996        request.referrer_policy = referrer_policy;
2997    }
2998}