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